239 lines
8.4 KiB
Python
239 lines
8.4 KiB
Python
|
|
"""
|
|||
|
|
Hauptfenster: verbindet Canvas, Toolbar, Properties-Panel und Statusleiste.
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
from PySide6.QtWidgets import (
|
|||
|
|
QMainWindow, QHBoxLayout, QWidget, QFileDialog, QMessageBox,
|
|||
|
|
)
|
|||
|
|
from PySide6.QtCore import Qt
|
|||
|
|
from PySide6.QtGui import QPixmap
|
|||
|
|
|
|||
|
|
from ui.canvas_widget import LogoCanvas
|
|||
|
|
from ui.properties_panel import PropertiesPanel
|
|||
|
|
from ui.toolbar import MainToolbar
|
|||
|
|
from ui.status_bar import LogoStatusBar
|
|||
|
|
from core.logo_model import LogoProject
|
|||
|
|
from core.image_loader import load_png, numpy_to_qpixmap
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MainWindow(QMainWindow):
|
|||
|
|
def __init__(self):
|
|||
|
|
super().__init__()
|
|||
|
|
self.setWindowTitle("Logo-Tool – Pixel-Perfect Editor")
|
|||
|
|
self.resize(1400, 900)
|
|||
|
|
self.setMinimumSize(900, 600)
|
|||
|
|
|
|||
|
|
self.project = LogoProject()
|
|||
|
|
|
|||
|
|
self._apply_dark_theme()
|
|||
|
|
self._build_ui()
|
|||
|
|
self._connect_signals()
|
|||
|
|
|
|||
|
|
def _apply_dark_theme(self):
|
|||
|
|
self.setStyleSheet("""
|
|||
|
|
QMainWindow {
|
|||
|
|
background-color: #252525;
|
|||
|
|
}
|
|||
|
|
QWidget {
|
|||
|
|
color: #ddd;
|
|||
|
|
font-family: "Segoe UI", "Inter", sans-serif;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
QScrollBar:vertical {
|
|||
|
|
background: #2a2a2a; width: 10px; border: none;
|
|||
|
|
}
|
|||
|
|
QScrollBar::handle:vertical {
|
|||
|
|
background: #555; border-radius: 4px; min-height: 30px;
|
|||
|
|
}
|
|||
|
|
QScrollBar:horizontal {
|
|||
|
|
background: #2a2a2a; height: 10px; border: none;
|
|||
|
|
}
|
|||
|
|
QScrollBar::handle:horizontal {
|
|||
|
|
background: #555; border-radius: 4px; min-width: 30px;
|
|||
|
|
}
|
|||
|
|
QDoubleSpinBox, QSpinBox {
|
|||
|
|
background: #333; border: 1px solid #555;
|
|||
|
|
border-radius: 3px; padding: 3px 6px; color: #ddd;
|
|||
|
|
}
|
|||
|
|
QSlider::groove:horizontal {
|
|||
|
|
background: #444; height: 4px; border-radius: 2px;
|
|||
|
|
}
|
|||
|
|
QSlider::handle:horizontal {
|
|||
|
|
background: #0078d4; width: 14px; height: 14px;
|
|||
|
|
margin: -5px 0; border-radius: 7px;
|
|||
|
|
}
|
|||
|
|
""")
|
|||
|
|
|
|||
|
|
def _build_ui(self):
|
|||
|
|
# Toolbar
|
|||
|
|
self.toolbar = MainToolbar(self)
|
|||
|
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolbar)
|
|||
|
|
|
|||
|
|
# Statusbar
|
|||
|
|
self.status_bar = LogoStatusBar(self)
|
|||
|
|
self.setStatusBar(self.status_bar)
|
|||
|
|
|
|||
|
|
# Zentrales Layout: Canvas + Properties-Panel
|
|||
|
|
central = QWidget()
|
|||
|
|
layout = QHBoxLayout(central)
|
|||
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|||
|
|
layout.setSpacing(0)
|
|||
|
|
|
|||
|
|
self.canvas = LogoCanvas()
|
|||
|
|
layout.addWidget(self.canvas, stretch=1)
|
|||
|
|
|
|||
|
|
self.properties = PropertiesPanel()
|
|||
|
|
layout.addWidget(self.properties)
|
|||
|
|
|
|||
|
|
self.setCentralWidget(central)
|
|||
|
|
|
|||
|
|
def _connect_signals(self):
|
|||
|
|
# Toolbar → Aktionen
|
|||
|
|
self.toolbar.open_file_requested.connect(self._on_open_file)
|
|||
|
|
self.toolbar.save_file_requested.connect(self._on_save_file)
|
|||
|
|
self.toolbar.zoom_to_fit_requested.connect(self.canvas.fit_to_view)
|
|||
|
|
self.toolbar.zoom_value_changed.connect(self.canvas.set_zoom)
|
|||
|
|
self.toolbar.show_original_toggled.connect(self._on_toggle_original)
|
|||
|
|
self.toolbar.show_vectors_toggled.connect(self._on_toggle_vectors)
|
|||
|
|
|
|||
|
|
# Canvas → Statusleiste
|
|||
|
|
self.canvas.zoom_changed.connect(self.status_bar.set_zoom)
|
|||
|
|
self.canvas.zoom_changed.connect(self.toolbar.update_zoom_display)
|
|||
|
|
|
|||
|
|
# Properties → Rendering
|
|||
|
|
self.properties.letter_spacing_changed.connect(self._on_spacing_changed)
|
|||
|
|
self.properties.stroke_width_changed.connect(self._on_stroke_changed)
|
|||
|
|
self.properties.retrace_requested.connect(self._on_retrace)
|
|||
|
|
self.properties.export_requested.connect(self._on_export)
|
|||
|
|
|
|||
|
|
# ─── Datei-Aktionen ──────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _on_open_file(self):
|
|||
|
|
path, _ = QFileDialog.getOpenFileName(
|
|||
|
|
self, "Logo-PNG öffnen", "",
|
|||
|
|
"Bilder (*.png *.jpg *.jpeg *.bmp);;Alle Dateien (*)",
|
|||
|
|
)
|
|||
|
|
if not path:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
pixels, w, h = load_png(path)
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "Fehler", f"Bild konnte nicht geladen werden:\n{e}")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self.project = LogoProject(
|
|||
|
|
source_path=Path(path),
|
|||
|
|
original_pixels=pixels,
|
|||
|
|
width=w,
|
|||
|
|
height=h,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
pixmap = numpy_to_qpixmap(pixels, w, h)
|
|||
|
|
self.canvas.set_original_image(pixmap)
|
|||
|
|
self.status_bar.set_file_info(Path(path).name, w, h)
|
|||
|
|
self.status_bar.set_path_count(0)
|
|||
|
|
|
|||
|
|
self._try_auto_trace()
|
|||
|
|
|
|||
|
|
def _on_save_file(self):
|
|||
|
|
if not self.project.has_image:
|
|||
|
|
QMessageBox.information(self, "Info", "Kein Bild geladen.")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
path, _ = QFileDialog.getSaveFileName(
|
|||
|
|
self, "Logo speichern", "", "PNG (*.png);;SVG (*.svg)",
|
|||
|
|
)
|
|||
|
|
if not path:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if path.lower().endswith(".png"):
|
|||
|
|
self._export_png(path)
|
|||
|
|
else:
|
|||
|
|
QMessageBox.information(
|
|||
|
|
self, "Info", "SVG-Export wird in einer zukünftigen Version unterstützt."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _on_export(self):
|
|||
|
|
self._on_save_file()
|
|||
|
|
|
|||
|
|
# ─── Rendering ───────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _on_spacing_changed(self, value: float):
|
|||
|
|
self.project.global_letter_spacing = value
|
|||
|
|
self._update_vector_overlay()
|
|||
|
|
|
|||
|
|
def _on_stroke_changed(self, value: float):
|
|||
|
|
self.project.global_stroke_width = value
|
|||
|
|
self._update_vector_overlay()
|
|||
|
|
|
|||
|
|
def _on_retrace(self):
|
|||
|
|
self._try_auto_trace()
|
|||
|
|
|
|||
|
|
def _try_auto_trace(self):
|
|||
|
|
"""Versucht die automatische Vektorisierung."""
|
|||
|
|
if not self.project.has_image:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from core.vectorizer import trace_to_paths
|
|||
|
|
except ImportError:
|
|||
|
|
QMessageBox.warning(
|
|||
|
|
self, "Warnung",
|
|||
|
|
"potracer ist nicht installiert.\n"
|
|||
|
|
"Bitte 'pip install potracer' ausführen für Vektorisierung.",
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
threshold = self.properties.slider_threshold.value()
|
|||
|
|
try:
|
|||
|
|
glyphs = trace_to_paths(self.project.original_pixels, threshold=threshold)
|
|||
|
|
self.project.glyphs = glyphs
|
|||
|
|
self.status_bar.set_path_count(len(glyphs))
|
|||
|
|
self.properties.populate_glyphs([g.label for g in glyphs])
|
|||
|
|
self._update_vector_overlay()
|
|||
|
|
except ImportError as e:
|
|||
|
|
QMessageBox.warning(self, "Warnung", str(e))
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "Tracing-Fehler", f"Vektorisierung fehlgeschlagen:\n{e}")
|
|||
|
|
|
|||
|
|
def _update_vector_overlay(self):
|
|||
|
|
"""Rendert die Vektorpfade neu und aktualisiert das Overlay."""
|
|||
|
|
if not self.project.has_vectors:
|
|||
|
|
self.canvas.clear_overlay()
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from core.cairo_renderer import render_to_qpixmap
|
|||
|
|
overlay = render_to_qpixmap(self.project)
|
|||
|
|
self.canvas.set_overlay(overlay)
|
|||
|
|
except Exception as e:
|
|||
|
|
self.status_bar.showMessage(f"Render-Fehler: {e}", 5000)
|
|||
|
|
|
|||
|
|
def _export_png(self, path: str):
|
|||
|
|
"""Exportiert das aktuelle Rendering als PNG."""
|
|||
|
|
try:
|
|||
|
|
from core.cairo_renderer import render_project
|
|||
|
|
import numpy as np
|
|||
|
|
from PIL import Image
|
|||
|
|
|
|||
|
|
pixels = render_project(self.project)
|
|||
|
|
img = Image.fromarray(pixels, "RGBA")
|
|||
|
|
img.save(path)
|
|||
|
|
self.status_bar.showMessage(f"Exportiert: {path}", 3000)
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "Export-Fehler", f"Export fehlgeschlagen:\n{e}")
|
|||
|
|
|
|||
|
|
# ─── Ansicht ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _on_toggle_original(self, visible: bool):
|
|||
|
|
if hasattr(self.canvas, '_pixmap_item') and self.canvas._pixmap_item:
|
|||
|
|
self.canvas._pixmap_item.setVisible(visible)
|
|||
|
|
|
|||
|
|
def _on_toggle_vectors(self, visible: bool):
|
|||
|
|
if hasattr(self.canvas, '_overlay_item') and self.canvas._overlay_item:
|
|||
|
|
self.canvas._overlay_item.setVisible(visible)
|