Line in icon tool

This commit is contained in:
2026-07-27 14:40:48 +02:00
parent c68e693359
commit e5968e165e
5 changed files with 187 additions and 22 deletions

View File

@@ -4,12 +4,12 @@ import math
from copy import deepcopy
from typing import Protocol
from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
from bedit_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID, Text
from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
class ShapeCreationTool(Protocol):
@@ -67,6 +67,45 @@ class TextCreationTool(RectangleCreationTool):
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
class LineCreationTool:
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
self.scene = scene
self.layer = layer
self.start: QPointF | None = None
self.preview: QGraphicsLineItem | None = None
def begin(self, position: QPointF) -> None:
position = self._bounded(position)
self.start = position
self.preview = self.scene.addLine(QLineF(position, position), QPen(Qt.PenStyle.DashLine))
def update(self, position: QPointF) -> None:
if self.preview is not None and self.start is not None:
self.preview.setLine(QLineF(self.start, self._bounded(position)))
def finish(self, position: QPointF) -> Shape | None:
if self.start is None:
return None
start = self.start
end = self._bounded(position)
self.cancel()
if start == end:
return None
return Line(layer=self.layer, pos=(round(start.x()), round(start.y())), end=(round(end.x()), round(end.y())))
def cancel(self) -> None:
if self.preview is not None:
self.scene.removeItem(self.preview)
self.preview = None
self.start = None
def _bounded(self, position: QPointF) -> QPointF:
rect = self.scene.sceneRect()
x = max(rect.left(), min(rect.right(), round(position.x())))
y = max(rect.top(), min(rect.bottom(), round(position.y())))
return QPointF(x, y)
class ShapeGraphicsItem(QGraphicsPathItem):
handle_size = 6.0
@@ -76,7 +115,7 @@ class ShapeGraphicsItem(QGraphicsPathItem):
self.shape_model = deepcopy(shape)
self.icon_scene = scene
self._original_shape: Shape | None = None
self._resizing = False
self._resize_handle: str | None = None
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
def resize_handle_rect(self) -> QRectF:
@@ -84,29 +123,32 @@ class ShapeGraphicsItem(QGraphicsPathItem):
corner = self.path().boundingRect().bottomRight()
return QRectF(corner.x() - size / 2, corner.y() - size / 2, size, size)
def resize_handles(self) -> dict[str, QRectF]:
return {"size": self.resize_handle_rect()}
def boundingRect(self) -> QRectF:
margin = self.handle_size / 2
return super().boundingRect().adjusted(-margin, -margin, margin, margin)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self._original_shape = self.current_shape()
self._resizing = self.isSelected() and self.resize_handle_rect().contains(event.pos())
if self._resizing:
self._resize_handle = next((name for name, rect in self.resize_handles().items() if self.isSelected() and rect.contains(event.pos())), None)
if self._resize_handle is not None:
event.accept()
return
super().mousePressEvent(event)
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self._resizing:
self.resize_to(event.scenePos())
if self._resize_handle is not None:
self.resize_to(event.scenePos(), self._resize_handle)
event.accept()
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self._resizing:
self.resize_to(event.scenePos())
self._resizing = False
if self._resize_handle is not None:
self.resize_to(event.scenePos(), self._resize_handle)
self._resize_handle = None
event.accept()
else:
super().mouseReleaseEvent(event)
@@ -130,9 +172,9 @@ class ShapeGraphicsItem(QGraphicsPathItem):
position = value
if isinstance(position, QPointF):
bounds = self.icon_scene.sceneRect()
size = self.path().boundingRect()
x = max(bounds.left(), min(bounds.right() - size.width(), round(position.x())))
y = max(bounds.top(), min(bounds.bottom() - size.height(), round(position.y())))
shape_rect = self.path().boundingRect()
x = max(bounds.left() - shape_rect.left(), min(bounds.right() - shape_rect.right(), round(position.x())))
y = max(bounds.top() - shape_rect.top(), min(bounds.bottom() - shape_rect.bottom(), round(position.y())))
return QPointF(x, y)
return super().itemChange(change, value)
@@ -141,12 +183,13 @@ class ShapeGraphicsItem(QGraphicsPathItem):
if self.isSelected():
painter.setPen(QPen(QColor("#ffffff")))
painter.setBrush(QBrush(QColor("#2675bf")))
painter.drawRect(self.resize_handle_rect())
for rect in self.resize_handles().values():
painter.drawRect(rect)
def current_shape(self) -> Shape:
raise NotImplementedError
def resize_to(self, position: QPointF) -> None:
def resize_to(self, position: QPointF, handle: str) -> None:
raise NotImplementedError
@@ -168,7 +211,7 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
shape.height = rect.height()
return shape
def resize_to(self, position: QPointF) -> None:
def resize_to(self, position: QPointF, _handle: str) -> None:
bounds = self.icon_scene.sceneRect()
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
@@ -199,7 +242,7 @@ class TextGraphicsItem(ShapeGraphicsItem):
shape.height = rect.height()
return shape
def resize_to(self, position: QPointF) -> None:
def resize_to(self, position: QPointF, _handle: str) -> None:
bounds = self.icon_scene.sceneRect()
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
@@ -223,6 +266,45 @@ class TextGraphicsItem(ShapeGraphicsItem):
self.setPath(path)
class LineGraphicsItem(ShapeGraphicsItem):
def __init__(self, shape_id: ShapeID, shape: Line, scene: IconGraphicsScene) -> None:
super().__init__(shape_id, shape, scene)
self.line = deepcopy(shape)
self._set_points(QPointF(shape.pos[0], shape.pos[1]), QPointF(shape.end[0], shape.end[1]))
self.setPen(scene._line_pen(shape.line_type, shape.line_thickness, shape.line_color))
self.setZValue(shape.layer)
def resize_handles(self) -> dict[str, QRectF]:
size = self.handle_size
start = self.path().pointAtPercent(0)
end = self.path().pointAtPercent(1)
return {"start": QRectF(start.x() - size / 2, start.y() - size / 2, size, size), "end": QRectF(end.x() - size / 2, end.y() - size / 2, size, size)}
def current_shape(self) -> Line:
shape = deepcopy(self.line)
start = self.mapToScene(self.path().pointAtPercent(0))
end = self.mapToScene(self.path().pointAtPercent(1))
shape.pos = (round(start.x()), round(start.y()))
shape.end = (round(end.x()), round(end.y()))
return shape
def resize_to(self, position: QPointF, handle: str) -> None:
bounds = self.icon_scene.sceneRect()
position = QPointF(max(bounds.left(), min(bounds.right(), round(position.x()))), max(bounds.top(), min(bounds.bottom(), round(position.y()))))
start = self.mapToScene(self.path().pointAtPercent(0))
end = self.mapToScene(self.path().pointAtPercent(1))
start = position if handle == "start" else start
end = position if handle == "end" else end
self.setPos(0, 0)
self._set_points(start, end)
def _set_points(self, start: QPointF, end: QPointF) -> None:
self.prepareGeometryChange()
path = QPainterPath(start)
path.lineTo(end)
self.setPath(path)
class PortGraphicsItem(QGraphicsRectItem):
size = 16.0
@@ -358,13 +440,19 @@ class IconGraphicsScene(QGraphicsScene):
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Text):
self.addItem(TextGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Line):
self.addItem(LineGraphicsItem(shape_id, shape, self))
@staticmethod
def _pen(shape: Rectangle) -> QPen:
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
@staticmethod
def _line_pen(line_type: LineType, thickness: float, color: str) -> QPen:
styles = {LineType.SOLID: Qt.PenStyle.SolidLine, LineType.DASHED: Qt.PenStyle.DashLine, LineType.DOTTED: Qt.PenStyle.DotLine, LineType.DASH_DOT: Qt.PenStyle.DashDotLine}
if shape.line_type is LineType.NONE:
if line_type is LineType.NONE:
return QPen(Qt.PenStyle.NoPen)
return QPen(IconGraphicsScene._color(shape.line_color), shape.line_thickness, styles[shape.line_type])
return QPen(IconGraphicsScene._color(color), thickness, styles[line_type])
@staticmethod
def _color(value: str) -> QColor: