fix some things

This commit is contained in:
2026-07-19 22:16:37 +02:00
parent dbca41640f
commit 8df5c2a8f9
7 changed files with 404 additions and 28 deletions

View File

@@ -144,8 +144,8 @@ Every component owns its ports, declarative icon, properties, and child graph:
"icon": { "icon": {
"size": {"width": 128, "height": 128}, "size": {"width": 128, "height": 128},
"elements": [{ "elements": [{
"type": "rectangle", "x": 1, "y": 1, "type": "rectangle", "x": 32, "y": 32,
"width": 126, "height": 126, "width": 64, "height": 64,
"cornerRadius": 5, "cornerRadius": 5,
"fill": "#dbeafe", "stroke": "#245c9c", "fill": "#dbeafe", "stroke": "#245c9c",
"lineWidth": 1.5, "lineStyle": "solid" "lineWidth": 1.5, "lineStyle": "solid"

View File

@@ -0,0 +1,162 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Untitled"
},
"roots": [
{
"id": "5ee742db-b25c-4d86-b4ca-cc0e61c4002a",
"name": "New Graph Block 1",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [],
"outputs": []
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 1.0,
"y": 1.0,
"width": 126.0,
"height": 126.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 8.0,
"y": 8.0,
"width": 112.0,
"height": 112.0,
"text": "Graph",
"color": "#202020",
"fontSize": 12.0
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [
{
"id": "89944479-e73e-446a-8d58-ebf35ac4144b",
"name": "New Graph Block 1",
"position": {
"x": 0.0,
"y": -96.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-e5cb81a5",
"name": "Port 1",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 64.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-eb0034af",
"name": "Port 2",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "Graph",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [],
"connections": []
}
}
}
],
"connections": []
}
}
}
]
}

View File

@@ -53,14 +53,14 @@ class Icon:
if not self.elements: if not self.elements:
self.elements = [{ self.elements = [{
"type": "ellipse" if self.shape == "ellipse" else "rectangle", "type": "ellipse" if self.shape == "ellipse" else "rectangle",
"x": 1.0, "y": 1.0, "width": self.width - 2.0, "height": self.height - 2.0, "x": 32.0, "y": 32.0, "width": 64.0, "height": 64.0,
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5, "fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
"lineStyle": "solid", "cornerRadius": 5.0, "lineStyle": "solid", "cornerRadius": 5.0,
}] }]
if self.text: if self.text:
self.elements.append({ self.elements.append({
"type": "text", "x": 8.0, "y": 8.0, "type": "text", "x": 40.0, "y": 40.0,
"width": self.width - 16.0, "height": self.height - 16.0, "width": 48.0, "height": 48.0,
"text": self.text, "color": "#202020", "fontSize": 12.0, "text": self.text, "color": "#202020", "fontSize": 12.0,
}) })

View File

@@ -4,6 +4,22 @@ from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygo
from bedit.document.model import Icon from bedit.document.model import Icon
def icon_bounds(icon: Icon) -> QRectF:
"""Return the automatic hitbox of all visible vector elements."""
bounds = QRectF()
for element in icon.elements:
if element.get("_deleted"):
continue
rect = QRectF(
float(element.get("x", 0)),
float(element.get("y", 0)),
max(0.0, float(element.get("width", 0))),
max(0.0, float(element.get("height", 0))),
)
bounds = rect if bounds.isNull() else bounds.united(rect)
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
def _pen(element: dict) -> QPen: def _pen(element: dict) -> QPen:
styles = { styles = {
"solid": Qt.PenStyle.SolidLine, "solid": Qt.PenStyle.SolidLine,
@@ -19,11 +35,19 @@ def _pen(element: dict) -> QPen:
) )
def paint_icon(painter: QPainter, icon: Icon, target: QRectF) -> None: def paint_icon(
painter: QPainter,
icon: Icon,
target: QRectF,
source: QRectF | None = None,
) -> None:
painter.save() painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setRenderHint(QPainter.RenderHint.Antialiasing)
if source is None:
source = QRectF(0, 0, icon.width, icon.height)
painter.translate(target.topLeft()) painter.translate(target.topLeft())
painter.scale(target.width() / icon.width, target.height() / icon.height) painter.scale(target.width() / source.width(), target.height() / source.height())
painter.translate(-source.left(), -source.top())
for element in icon.elements: for element in icon.elements:
kind = element.get("type", "rectangle") kind = element.get("type", "rectangle")
rect = QRectF( rect = QRectF(
@@ -55,12 +79,20 @@ def icon_pixmap(icon: Icon, size: int = 16) -> QPixmap:
pixmap = QPixmap(size, size) pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent) pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap) painter = QPainter(pixmap)
ratio = min(size / icon.width, size / icon.height) bounds = icon_bounds(icon)
width, height = icon.width * ratio, icon.height * ratio ratio = min(size / bounds.width(), size / bounds.height())
paint_icon(painter, icon, QRectF((size - width) / 2, (size - height) / 2, width, height)) width, height = bounds.width() * ratio, bounds.height() * ratio
paint_icon(
painter,
icon,
QRectF((size - width) / 2, (size - height) / 2, width, height),
bounds,
)
painter.end() painter.end()
return pixmap return pixmap
def library_icon(icon: Icon) -> QIcon: def library_icon(icon: Icon) -> QIcon:
return QIcon(icon_pixmap(icon, 16)) # Render large enough for the tall Libraries rows. icon_pixmap crops to the
# vector hitbox first, so a 32x32 drawing is shown as large as a 128x128 one.
return QIcon(icon_pixmap(icon, 28))

View File

@@ -31,6 +31,10 @@ class MainWindow(QMainWindow):
super().__init__() super().__init__()
self.ui = Ui_MainWindow() self.ui = Ui_MainWindow()
self.ui.setupUi(self) self.ui.setupUi(self)
self.ui.emptyPage.setStyleSheet("background-color: #9a9a9a;")
self.ui.emptyWorkspaceLabel.setStyleSheet(
"background: transparent; color: #202020;"
)
self._create_camera_toolbar() self._create_camera_toolbar()
self.settings = QSettings() self.settings = QSettings()
@@ -63,7 +67,8 @@ class MainWindow(QMainWindow):
def _configure_models(self) -> None: def _configure_models(self) -> None:
self.ui.treeView.setModel(self.library_tree_model) self.ui.treeView.setModel(self.library_tree_model)
self.ui.treeView.setIconSize(QSize(16, 16)) self.ui.treeView.setIconSize(QSize(28, 28))
self.ui.treeView.setStyleSheet("QTreeView::item { height: 32px; }")
self.ui.treeView.setHeaderHidden(True) self.ui.treeView.setHeaderHidden(True)
self.ui.treeView.setDragEnabled(True) self.ui.treeView.setDragEnabled(True)
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly) self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
@@ -379,6 +384,7 @@ class MainWindow(QMainWindow):
scene = self.ui.graphView.scene() scene = self.ui.graphView.scene()
if scene is not None: if scene is not None:
scene.update() scene.update()
self.ui.graphView.viewport().update()
def _document_opened_changed(self, opened: bool) -> None: def _document_opened_changed(self, opened: bool) -> None:
for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs): for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs):

View File

@@ -31,7 +31,7 @@ from PySide6.QtWidgets import (
from bedit.document.controller import DocumentController from bedit.document.controller import DocumentController
from bedit.document.model import Component, Connection, Endpoint, Port from bedit.document.model import Component, Connection, Endpoint, Port
from bedit.library.tree_model import COMPONENT_MIME_TYPE from bedit.library.tree_model import COMPONENT_MIME_TYPE
from bedit.icon_renderer import paint_icon from bedit.icon_renderer import icon_bounds, paint_icon
SELECTION_MIME_TYPE = "application/x-bedit-selection" SELECTION_MIME_TYPE = "application/x-bedit-selection"
@@ -67,6 +67,7 @@ class ComponentGraphicsItem(QGraphicsObject):
self.component = component self.component = component
self.controller = controller self.controller = controller
self.drag_start = QPointF() self.drag_start = QPointF()
self.hitbox = icon_bounds(component.icon)
self.setFlags( self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
@@ -74,7 +75,7 @@ class ComponentGraphicsItem(QGraphicsObject):
) )
self.input_ports = self._create_ports(component.inputs, "target", 0.0) self.input_ports = self._create_ports(component.inputs, "target", 0.0)
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH) self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
self.setTransformOriginPoint(self.WIDTH / 2, self.HEIGHT / 2) self.setTransformOriginPoint(self.hitbox.center())
self.setRotation(component.rotation) self.setRotation(component.rotation)
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]: def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
@@ -92,7 +93,7 @@ class ComponentGraphicsItem(QGraphicsObject):
return result return result
def boundingRect(self) -> QRectF: # noqa: N802 def boundingRect(self) -> QRectF: # noqa: N802
return QRectF(0, 0, self.WIDTH, self.HEIGHT) return self.hitbox
def paint( def paint(
self, self,
@@ -101,7 +102,11 @@ class ComponentGraphicsItem(QGraphicsObject):
widget: QWidget | None = None, widget: QWidget | None = None,
) -> None: ) -> None:
del option, widget del option, widget
paint_icon(painter, self.component.icon, self.boundingRect()) paint_icon(
painter,
self.component.icon,
QRectF(0, 0, self.WIDTH, self.HEIGHT),
)
if self.isSelected(): if self.isSelected():
painter.setBrush(Qt.BrushStyle.NoBrush) painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine)) painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine))
@@ -333,17 +338,8 @@ class GraphScene(QGraphicsScene):
self.update_connection(connection.id) self.update_connection(connection.id)
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802 def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
painter.fillRect(rect, QColor("#e4e4e4")) # The view paints the grid so it always covers the complete viewport.
if self.controller.document is None or self.controller.active_component is None: del painter, rect
return
spacing = _graph_grid_size()
left = int(rect.left()) - (int(rect.left()) % spacing)
top = int(rect.top()) - (int(rect.top()) % spacing)
painter.setPen(QPen(QColor("#b9c0c7"), 0))
for x in range(left, int(rect.right()) + spacing, spacing):
painter.drawLine(x, rect.top(), x, rect.bottom())
for y in range(top, int(rect.bottom()) + spacing, spacing):
painter.drawLine(rect.left(), y, rect.right(), y)
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None: def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None:
if endpoint.interface is not None: if endpoint.interface is not None:
@@ -412,10 +408,28 @@ class GraphWorkspaceView(QGraphicsView):
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.setRenderHint(QPainter.RenderHint.Antialiasing) self.setRenderHint(QPainter.RenderHint.Antialiasing)
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
self.setBackgroundBrush(QColor("#9a9a9a")) self.setBackgroundBrush(QColor("#f7f7f7"))
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter) self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
"""Paint the visible graph viewport in scene coordinates."""
painter.fillRect(rect, QColor("#f7f7f7"))
if (
self.controller is None
or self.controller.document is None
or self.controller.active_component is None
):
return
spacing = _graph_grid_size()
left = int(rect.left()) - (int(rect.left()) % spacing)
top = int(rect.top()) - (int(rect.top()) % spacing)
painter.setPen(QPen(QColor("#c5cbd1"), 0))
for x in range(left, int(rect.right()) + spacing, spacing):
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
for y in range(top, int(rect.bottom()) + spacing, spacing):
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
def _zoom(self, factor: float) -> None: def _zoom(self, factor: float) -> None:
current = self.transform().m11() current = self.transform().m11()
target = current * factor target = current * factor

162
BEdit/test.bedit.json Normal file
View File

@@ -0,0 +1,162 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Untitled"
},
"roots": [
{
"id": "5ee742db-b25c-4d86-b4ca-cc0e61c4002a",
"name": "New Graph Block 1",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [],
"outputs": []
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 1.0,
"y": 1.0,
"width": 126.0,
"height": 126.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 8.0,
"y": 8.0,
"width": 112.0,
"height": 112.0,
"text": "Graph",
"color": "#202020",
"fontSize": 12.0
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [
{
"id": "89944479-e73e-446a-8d58-ebf35ac4144b",
"name": "New Graph Block 1",
"position": {
"x": 0.0,
"y": -96.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-e5cb81a5",
"name": "Port 1",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 64.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-eb0034af",
"name": "Port 2",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "Graph",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [],
"connections": []
}
}
}
],
"connections": []
}
}
}
]
}