from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from typing import Iterable import copy @dataclass class CompositionInfo: width: int = 1200 height: int = 600 @dataclass class SourceInfo: original_export: str = "" derived_from: str = "" composition: CompositionInfo = field(default_factory=CompositionInfo) @dataclass class LogicalDisplayConfig: rows: int = 3 cols: int = 6 preview_width: int = 1200 preview_height: int = 600 tile_width: int = 200 tile_height: int = 200 @dataclass class DefaultsConfig: protocol: str = "artnet" subnet: int = 0 color_format: str = "rgb" tile_behavior: str = "solid_color_per_tile" global_gamma: float = 2.2 @dataclass class CalibrationConfig: brightness: float = 1.0 red_gain: float = 1.0 green_gain: float = 1.0 blue_gain: float = 1.0 @dataclass class SegmentConfig: name: str side: str start_channel: int led_count: int orientation_rad: float = 0.0 x0: float = 0.0 y0: float = 0.0 x1: float = 0.0 y1: float = 0.0 reverse: bool = False @dataclass class TileConfig: tile_id: str row: int col: int screen_name: str = "" controller_ip: str = "" controller_host: str = "" controller_name: str = "" controller_mac: str = "" universe: int = 0 subnet: int = 0 led_total: int = 0 x0: float = 0.0 y0: float = 0.0 x1: float = 0.0 y1: float = 0.0 enabled: bool = True calibration: CalibrationConfig = field(default_factory=CalibrationConfig) segments: list[SegmentConfig] = field(default_factory=list) @property def brightness_factor(self) -> float: return self.calibration.brightness @brightness_factor.setter def brightness_factor(self, value: float) -> None: self.calibration.brightness = value @dataclass class InfinityMirrorConfig: name: str = "Infinity Mirror" version: str = "1.0" source: SourceInfo = field(default_factory=SourceInfo) logical_display: LogicalDisplayConfig = field(default_factory=LogicalDisplayConfig) defaults: DefaultsConfig = field(default_factory=DefaultsConfig) tiles: list[TileConfig] = field(default_factory=list) file_path: Path | None = None def clone(self) -> "InfinityMirrorConfig": return copy.deepcopy(self) def sorted_tiles(self) -> list[TileConfig]: return sorted(self.tiles, key=lambda tile: (tile.row, tile.col, tile.tile_id)) def tile_lookup(self) -> dict[str, TileConfig]: return {tile.tile_id: tile for tile in self.tiles} def tile_at(self, row: int, col: int) -> TileConfig | None: for tile in self.tiles: if tile.row == row and tile.col == col: return tile return None def all_segments(self) -> Iterable[tuple[TileConfig, SegmentConfig]]: for tile in self.sorted_tiles(): for segment in tile.segments: yield tile, segment