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)