275 lines
9.8 KiB
Python
275 lines
9.8 KiB
Python
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)
|