Reorganized the application around a clear frontend/backend boundary.

src/bedit/
├── __main__.py
├── core/                   # Pure Python, no PySide
│   ├── model.py
│   ├── port_types.py
│   ├── serializer.py
│   └── libraries.py
└── gui/                    # All Qt-dependent code
    ├── app.py
    ├── main_window.py
    ├── preferences.py
    ├── controllers/
    ├── dialogs/
    ├── graphics/
    ├── models/
    └── generated/          # Designer/resource output only
Notable improvements:
Domain models, serialization, port types, and library parsing are now Qt-free.
Qt signals and undo infrastructure are explicitly isolated under gui/controllers.
Library parsing is separated from the Qt repository and tree models.
All generated Python is contained in gui/generated.
Designer build tasks now write to the generated directory.
The application entry point and package metadata use the new paths.
README now documents the structure and dependency rules.
Removed the old mixed document, library, and workspace packages.
This commit is contained in:
2026-07-20 12:09:00 +02:00
parent 1a47952358
commit 48a2b4c8d0
43 changed files with 1011 additions and 310 deletions

View File

@@ -0,0 +1,98 @@
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF
from bedit.core.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:
styles = {
"solid": Qt.PenStyle.SolidLine,
"dash": Qt.PenStyle.DashLine,
"dot": Qt.PenStyle.DotLine,
"dash-dot": Qt.PenStyle.DashDotLine,
"none": Qt.PenStyle.NoPen,
}
return QPen(
QColor(element.get("stroke", "#303030")),
float(element.get("lineWidth", 1.5)),
styles.get(element.get("lineStyle", "solid"), Qt.PenStyle.SolidLine),
)
def paint_icon(
painter: QPainter,
icon: Icon,
target: QRectF,
source: QRectF | None = None,
) -> None:
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
if source is None:
source = QRectF(0, 0, icon.width, icon.height)
painter.translate(target.topLeft())
painter.scale(target.width() / source.width(), target.height() / source.height())
painter.translate(-source.left(), -source.top())
for element in icon.elements:
kind = element.get("type", "rectangle")
rect = QRectF(
float(element.get("x", 0)), float(element.get("y", 0)),
float(element.get("width", 20)), float(element.get("height", 20)),
)
painter.setPen(_pen(element))
fill = element.get("fill", "#ffffff")
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill))
if kind == "rectangle":
radius = float(element.get("cornerRadius", 0))
painter.drawRoundedRect(rect, radius, radius)
elif kind in {"circle", "ellipse"}:
painter.drawEllipse(rect)
elif kind == "line":
painter.drawLine(rect.topLeft(), rect.bottomRight())
elif kind == "triangle":
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]))
elif kind == "text":
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
font = QFont()
font.setPointSizeF(float(element.get("fontSize", 12)))
painter.setFont(font)
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(element.get("text", "Text")))
painter.restore()
def icon_pixmap(icon: Icon, size: int = 16) -> QPixmap:
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
bounds = icon_bounds(icon)
ratio = min(size / bounds.width(), size / bounds.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()
return pixmap
def library_icon(icon: Icon) -> QIcon:
# 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))