First upload, 18 controller version
This commit is contained in:
274
app/ui/preview_painter.py
Normal file
274
app/ui/preview_painter.py
Normal file
@@ -0,0 +1,274 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from app.qt_compat import QColor, QFont, QLinearGradient, QPainter, QPainterPath, QPen, QPointF, QRectF, Qt
|
||||
|
||||
from app.config.models import SegmentConfig, TileConfig
|
||||
from app.core.geometry import segment_led_positions
|
||||
from app.core.types import PreviewFrame, RGBColor
|
||||
|
||||
from .preview_layout import PreviewLayout, segment_display_rect
|
||||
from .preview_modes import preview_mode_flags
|
||||
|
||||
|
||||
def _qcolor(color: RGBColor, alpha: float = 1.0) -> QColor:
|
||||
red, green, blue = color.to_8bit_tuple()
|
||||
qt_color = QColor(red, green, blue)
|
||||
qt_color.setAlphaF(max(0.0, min(1.0, alpha)))
|
||||
return qt_color
|
||||
|
||||
|
||||
def paint_empty_preview(painter: QPainter, rect: QRectF) -> None:
|
||||
painter.fillRect(rect, QColor("#1E1E1E"))
|
||||
painter.setPen(QColor("#8C8C8C"))
|
||||
painter.drawText(rect, Qt.AlignCenter, "Open a mapping to start the preview.")
|
||||
|
||||
|
||||
def paint_preview_scene(
|
||||
painter: QPainter,
|
||||
*,
|
||||
config,
|
||||
frame: PreviewFrame,
|
||||
preview_mode: str,
|
||||
selected_tile_id: str | None,
|
||||
target_rect: QRectF,
|
||||
layout: PreviewLayout,
|
||||
) -> None:
|
||||
flags = preview_mode_flags(preview_mode)
|
||||
background = QLinearGradient(0, 0, target_rect.width(), target_rect.height())
|
||||
background.setColorAt(0.0, _qcolor(frame.background_start))
|
||||
background.setColorAt(1.0, _qcolor(frame.background_end))
|
||||
painter.fillRect(target_rect, background)
|
||||
|
||||
_draw_canvas_shell(painter, layout.canvas_rect)
|
||||
for tile in config.sorted_tiles():
|
||||
tile_frame = frame.tiles.get(tile.tile_id)
|
||||
tile_rect = layout.tile_rects[tile.tile_id]
|
||||
_draw_tile(
|
||||
painter,
|
||||
tile=tile,
|
||||
tile_frame=tile_frame,
|
||||
rect=tile_rect,
|
||||
flags=flags,
|
||||
selected_tile_id=selected_tile_id,
|
||||
)
|
||||
|
||||
if flags["show_overlay_title"]:
|
||||
painter.setPen(QColor(204, 204, 204, 140))
|
||||
painter.drawText(
|
||||
target_rect.adjusted(24, 18, -24, -18),
|
||||
Qt.AlignTop | Qt.AlignRight,
|
||||
"Technical Preview",
|
||||
)
|
||||
|
||||
|
||||
def _draw_canvas_shell(painter: QPainter, rect: QRectF) -> None:
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(rect, 8, 8)
|
||||
painter.fillPath(path, QColor("#252526"))
|
||||
painter.setPen(QPen(QColor("#3C3C3C"), 1.0))
|
||||
painter.drawPath(path)
|
||||
|
||||
|
||||
def _draw_tile(
|
||||
painter: QPainter,
|
||||
*,
|
||||
tile: TileConfig,
|
||||
tile_frame,
|
||||
rect: QRectF,
|
||||
flags: dict[str, bool],
|
||||
selected_tile_id: str | None,
|
||||
) -> None:
|
||||
if tile_frame is None:
|
||||
return
|
||||
|
||||
base = min(rect.width(), rect.height())
|
||||
rounding = max(4.0, base * 0.045)
|
||||
fill_color = _qcolor(tile_frame.fill_color)
|
||||
rim_color = _qcolor(tile_frame.rim_color)
|
||||
diagonal_split = tile_frame.metadata.get("diagonal_split")
|
||||
|
||||
tile_path = QPainterPath()
|
||||
tile_path.addRoundedRect(rect, rounding, rounding)
|
||||
if flags["show_fill"] and isinstance(diagonal_split, dict):
|
||||
_draw_diagonal_split_fill(painter, tile_path, rect, diagonal_split)
|
||||
elif flags["show_fill"]:
|
||||
painter.fillPath(tile_path, fill_color)
|
||||
else:
|
||||
painter.fillPath(tile_path, QColor("#090B12"))
|
||||
|
||||
if flags["show_fill"]:
|
||||
highlight = QLinearGradient(rect.topLeft(), rect.bottomLeft())
|
||||
highlight.setColorAt(0.0, QColor(255, 255, 255, 26))
|
||||
highlight.setColorAt(0.12, QColor(255, 255, 255, 10))
|
||||
highlight.setColorAt(1.0, QColor(0, 0, 0, 0))
|
||||
painter.fillPath(tile_path, highlight)
|
||||
|
||||
outline_color = rim_color if flags["show_fill"] else QColor(255, 255, 255, 32)
|
||||
painter.setPen(QPen(outline_color, 1.2 if flags["show_leds"] else 1.0))
|
||||
painter.drawPath(tile_path)
|
||||
|
||||
if flags["show_fill"]:
|
||||
inner_rect = rect.adjusted(rect.width() * 0.08, rect.height() * 0.08, -rect.width() * 0.08, -rect.height() * 0.08)
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 14), 1.0))
|
||||
painter.drawRoundedRect(inner_rect, rounding * 0.66, rounding * 0.66)
|
||||
|
||||
if not tile.enabled:
|
||||
painter.fillPath(tile_path, QColor(0, 0, 0, 125))
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 36), 1.0, Qt.DashLine))
|
||||
painter.drawRoundedRect(rect.adjusted(6, 6, -6, -6), rounding * 0.8, rounding * 0.8)
|
||||
|
||||
if selected_tile_id == tile.tile_id:
|
||||
painter.setPen(QPen(QColor("#007ACC"), 2.0))
|
||||
painter.drawRoundedRect(rect.adjusted(-3, -3, 3, 3), rounding + 2, rounding + 2)
|
||||
|
||||
if flags["show_labels"]:
|
||||
_draw_labels(painter, tile, tile_frame, rect, technical_meta=flags["show_technical_meta"])
|
||||
|
||||
if flags["show_leds"]:
|
||||
_draw_segment_preview(
|
||||
painter,
|
||||
tile,
|
||||
tile_frame,
|
||||
rect,
|
||||
show_guides=flags["show_guides"],
|
||||
show_direction=flags["show_direction"],
|
||||
)
|
||||
|
||||
|
||||
def _draw_diagonal_split_fill(painter: QPainter, tile_path: QPainterPath, rect: QRectF, diagonal_split: dict[str, object]) -> None:
|
||||
color_a = diagonal_split.get("color_a")
|
||||
color_b = diagonal_split.get("color_b")
|
||||
if not isinstance(color_a, RGBColor) or not isinstance(color_b, RGBColor):
|
||||
painter.fillPath(tile_path, QColor("#000000"))
|
||||
return
|
||||
|
||||
painter.save()
|
||||
painter.setClipPath(tile_path)
|
||||
orientation = str(diagonal_split.get("orientation", "slash"))
|
||||
|
||||
first = QPainterPath()
|
||||
second = QPainterPath()
|
||||
if orientation == "backslash":
|
||||
first.moveTo(rect.topLeft())
|
||||
first.lineTo(rect.topRight())
|
||||
first.lineTo(rect.bottomRight())
|
||||
first.closeSubpath()
|
||||
|
||||
second.moveTo(rect.topLeft())
|
||||
second.lineTo(rect.bottomLeft())
|
||||
second.lineTo(rect.bottomRight())
|
||||
second.closeSubpath()
|
||||
else:
|
||||
first.moveTo(rect.topLeft())
|
||||
first.lineTo(rect.topRight())
|
||||
first.lineTo(rect.bottomLeft())
|
||||
first.closeSubpath()
|
||||
|
||||
second.moveTo(rect.topRight())
|
||||
second.lineTo(rect.bottomRight())
|
||||
second.lineTo(rect.bottomLeft())
|
||||
second.closeSubpath()
|
||||
|
||||
painter.fillPath(first, _qcolor(color_a))
|
||||
painter.fillPath(second, _qcolor(color_b))
|
||||
painter.restore()
|
||||
|
||||
|
||||
def _draw_labels(painter: QPainter, tile: TileConfig, tile_frame, rect: QRectF, technical_meta: bool = False) -> None:
|
||||
painter.save()
|
||||
base = min(rect.width(), rect.height())
|
||||
horizontal_padding = max(12.0, rect.width() * 0.08)
|
||||
top_padding = max(10.0, rect.height() * 0.07)
|
||||
bottom_padding = max(12.0, rect.height() * 0.08)
|
||||
|
||||
font = QFont()
|
||||
font.setPointSizeF(max(14.0, base * 0.105))
|
||||
font.setWeight(QFont.DemiBold)
|
||||
painter.setFont(font)
|
||||
painter.setPen(_qcolor(tile_frame.label_color, 0.92))
|
||||
title_rect = rect.adjusted(horizontal_padding, top_padding, -horizontal_padding, -rect.height() * 0.52)
|
||||
painter.drawText(title_rect, Qt.AlignLeft | Qt.AlignTop | Qt.TextWordWrap, tile.tile_id)
|
||||
|
||||
meta_font = QFont(font)
|
||||
meta_font.setPointSizeF(max(11.5, base * (0.07 if technical_meta else 0.082)))
|
||||
meta_font.setWeight(QFont.Normal)
|
||||
painter.setFont(meta_font)
|
||||
text = f"R{tile.row} C{tile.col}"
|
||||
if technical_meta:
|
||||
text = f"{tile.screen_name or tile.controller_ip}\nU{tile.universe} S{tile.subnet} {tile.led_total} LEDs"
|
||||
painter.setPen(QColor(235, 244, 249, 165))
|
||||
meta_rect = rect.adjusted(horizontal_padding, rect.height() * 0.56, -horizontal_padding, -bottom_padding)
|
||||
painter.drawText(meta_rect, Qt.AlignLeft | Qt.AlignBottom | Qt.TextWordWrap, text)
|
||||
painter.restore()
|
||||
|
||||
|
||||
def _draw_segment_preview(
|
||||
painter: QPainter,
|
||||
tile: TileConfig,
|
||||
tile_frame,
|
||||
rect: QRectF,
|
||||
*,
|
||||
show_guides: bool,
|
||||
show_direction: bool,
|
||||
) -> None:
|
||||
painter.save()
|
||||
led_radius = max(2.0, min(rect.width(), rect.height()) / 64.0)
|
||||
guide_pen = QPen(QColor(220, 228, 236, 46), max(0.9, led_radius * 0.55))
|
||||
guide_pen.setCapStyle(Qt.RoundCap)
|
||||
guide_pen.setJoinStyle(Qt.RoundJoin)
|
||||
for segment in tile.segments:
|
||||
points = _segment_points(tile, segment, rect)
|
||||
colors = tile_frame.led_pixels.get(segment.name, [])
|
||||
if show_guides and len(points) >= 2:
|
||||
painter.setPen(guide_pen)
|
||||
for start, end in zip(points, points[1:]):
|
||||
painter.drawLine(start, end)
|
||||
|
||||
painter.setPen(Qt.NoPen)
|
||||
for index, point in enumerate(points):
|
||||
color = colors[index] if index < len(colors) else tile_frame.rim_color
|
||||
if color.to_8bit_tuple() == (0, 0, 0):
|
||||
continue
|
||||
painter.setBrush(_qcolor(color, 0.94))
|
||||
painter.drawEllipse(point, led_radius, led_radius)
|
||||
|
||||
if show_direction and points:
|
||||
_draw_direction_arrow(painter, points, segment)
|
||||
|
||||
painter.restore()
|
||||
|
||||
|
||||
def _segment_points(tile: TileConfig, segment: SegmentConfig, rect: QRectF) -> list[QPointF]:
|
||||
display_rect = segment_display_rect(tile, rect)
|
||||
inset = max(2.0, min(display_rect.width(), display_rect.height()) * 0.02)
|
||||
insets = (
|
||||
inset / max(1.0, display_rect.width()),
|
||||
inset / max(1.0, display_rect.height()),
|
||||
)
|
||||
return [
|
||||
QPointF(display_rect.left() + x_pos * display_rect.width(), display_rect.top() + y_pos * display_rect.height())
|
||||
for x_pos, y_pos in segment_led_positions(tile, segment, insets=insets)
|
||||
]
|
||||
|
||||
|
||||
def _draw_direction_arrow(painter: QPainter, points: list[QPointF], segment: SegmentConfig) -> None:
|
||||
if len(points) < 2:
|
||||
return
|
||||
start = points[0]
|
||||
end = points[-1]
|
||||
|
||||
mid = QPointF((start.x() + end.x()) / 2.0, (start.y() + end.y()) / 2.0)
|
||||
dx = end.x() - start.x()
|
||||
dy = end.y() - start.y()
|
||||
length = math.hypot(dx, dy) or 1.0
|
||||
ux, uy = dx / length, dy / length
|
||||
arrow_len = 14.0
|
||||
left = QPointF(mid.x() - ux * arrow_len + -uy * arrow_len * 0.5, mid.y() - uy * arrow_len + ux * arrow_len * 0.5)
|
||||
right = QPointF(mid.x() - ux * arrow_len - -uy * arrow_len * 0.5, mid.y() - uy * arrow_len - ux * arrow_len * 0.5)
|
||||
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 85), 1.0))
|
||||
painter.drawLine(start, end)
|
||||
painter.drawLine(mid, left)
|
||||
painter.drawLine(mid, right)
|
||||
Reference in New Issue
Block a user