from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Iterable from app.config.models import InfinityMirrorConfig, TileConfig from app.core.types import PatternParameters, TilePatternSample, clamp @dataclass(frozen=True) class ParameterSpec: key: str label: str kind: str minimum: float = 0.0 maximum: float = 1.0 step: float = 0.01 reset_value: float | None = None options: tuple[tuple[str, str], ...] = () tooltip: str = "" @dataclass(frozen=True) class PatternDescriptor: pattern_id: str display_name: str description: str supported_parameters: tuple[str, ...] accent_hex: str = "#4D7CFF" temporal_profile: str = "smooth" @dataclass class PatternContext: config: InfinityMirrorConfig params: PatternParameters time_s: float tempo_bpm: float = 60.0 tempo_phase: float = 0.0 @property def rows(self) -> int: return self.config.logical_display.rows @property def cols(self) -> int: return self.config.logical_display.cols def sorted_tiles(self) -> list[TileConfig]: return self.config.sorted_tiles() @property def tempo_hz(self) -> float: return max(0.05, float(self.tempo_bpm) / 60.0) @property def tempo_multiplier(self) -> float: return clamp(float(self.params.tempo_multiplier), 0.25, 8.0) @property def pattern_tempo_hz(self) -> float: return self.tempo_hz * self.tempo_multiplier @property def pattern_tempo_phase(self) -> float: return self.tempo_phase * self.tempo_multiplier class BasePattern(ABC): descriptor: PatternDescriptor @abstractmethod def render(self, context: PatternContext) -> dict[str, TilePatternSample]: raise NotImplementedError class PatternRegistry: def __init__(self, patterns: Iterable[BasePattern]) -> None: self._patterns = {pattern.descriptor.pattern_id: pattern for pattern in patterns} def get(self, pattern_id: str) -> BasePattern: return self._patterns[pattern_id] def descriptors(self) -> list[PatternDescriptor]: return [self._patterns[key].descriptor for key in sorted(self._patterns)] def ids(self) -> list[str]: return list(sorted(self._patterns)) COMMON_PARAMETER_SPECS: dict[str, ParameterSpec] = { "brightness": ParameterSpec("brightness", "Brightness", "slider", 0.0, 2.0, 0.01, reset_value=1.0, tooltip="Pattern output level."), "fade": ParameterSpec("fade", "Smoothing", "slider", 0.0, 1.0, 0.01, reset_value=0.0, tooltip="Higher values create softer transitions."), "tempo_multiplier": ParameterSpec( "tempo_multiplier", "Tempo Multiplier", "slider", 0.25, 8.0, 0.05, reset_value=1.0, tooltip="Scales this pattern relative to the global BPM.", ), "direction": ParameterSpec( "direction", "Direction", "combo", options=( ("left_to_right", "Left to Right"), ("right_to_left", "Right to Left"), ("top_to_bottom", "Top to Bottom"), ("bottom_to_top", "Bottom to Top"), ("outward", "Outward"), ("inward", "Inward"), ), tooltip="Primary motion direction.", ), "checker_mode": ParameterSpec( "checker_mode", "Checker Mode", "combo", options=( ("classic", "Classic"), ("diagonal", "Diagonal Split"), ("checkerd", "Checkerd"), ), tooltip="Classic checker, diagonal half-pixels, or diagonal flip animation.", ), "scan_style": ParameterSpec( "scan_style", "Scan Style", "combo", options=( ("line", "Line"), ("bands", "Bands"), ), tooltip="Single moving scan band or repeating band pattern.", ), "angle": ParameterSpec( "angle", "Angle", "angle", minimum=0.0, maximum=315.0, step=45.0, tooltip="Scan direction in 45 degree steps.", ), "on_width": ParameterSpec( "on_width", "On Width", "slider", 0.1, 2.0, 0.05, tooltip="Length of the active scan window.", ), "off_width": ParameterSpec( "off_width", "Off Width", "slider", 0.1, 2.0, 0.05, tooltip="Gap between active scan windows.", ), "band_thickness": ParameterSpec( "band_thickness", "Band Thickness", "slider", 0.1, 2.0, 0.05, tooltip="Visible thickness of the lit band inside the active window.", ), "flip_horizontal": ParameterSpec( "flip_horizontal", "Flip Horizontal", "checkbox", tooltip="Mirror scan evaluation left-to-right for installation alignment.", ), "flip_vertical": ParameterSpec( "flip_vertical", "Flip Vertical", "checkbox", tooltip="Mirror scan evaluation top-to-bottom for installation alignment.", ), "strobe_mode": ParameterSpec( "strobe_mode", "Strobe Mode", "combo", options=( ("global", "Global"), ("random_pixels", "Random Pixels"), ("random_leds", "Random LEDs"), ), tooltip="Whole-wall strobe, grouped random pixel blocks, or fully shuffled per-LED timing.", ), "stopwatch_mode": ParameterSpec( "stopwatch_mode", "Stopwatch Mode", "combo", options=( ("sync", "Sync"), ("random", "Random"), ), tooltip="Run all tiles together or with deterministic random offsets.", ), "color_mode": ParameterSpec( "color_mode", "Color Mode", "combo", options=( ("dual", "Dual"), ("palette", "Palette"), ("mono", "Mono"), ("complementary", "Complementary"), ("random_colors", "Random Colors"), ("custom_random", "Custom Random"), ), tooltip="How colors are chosen for the pattern.", ), "primary_color": ParameterSpec("primary_color", "Primary Color", "color", tooltip="Main color."), "secondary_color": ParameterSpec("secondary_color", "Secondary Color", "color", tooltip="Secondary color."), "palette": ParameterSpec("palette", "Palette", "combo", tooltip="Palette for palette-driven patterns."), "symmetry": ParameterSpec( "symmetry", "Mirror", "combo", options=(("none", "None"), ("horizontal", "Horizontal"), ("vertical", "Vertical"), ("both", "Both")), tooltip="Mirrors pattern coordinates around the center.", ), "center_pulse_mode": ParameterSpec( "center_pulse_mode", "Pulse Mode", "combo", options=( ("expand", "Expand"), ("reverse", "Reverse"), ("outline", "Outline"), ("outline_reverse", "Outline Reverse"), ), tooltip="Expand from the center, run inward, or use only the rectangular outline rings.", ), "block_size": ParameterSpec("block_size", "Block Size", "slider", 0.1, 6.0, 0.1, tooltip="Width of active bands."), "pixel_group_size": ParameterSpec( "pixel_group_size", "Pixel Group", "slider", 1.0, 5.0, 1.0, reset_value=1.0, tooltip="Treat several adjacent LEDs as one strobe pixel.", ), "strobe_duty_cycle": ParameterSpec( "strobe_duty_cycle", "Duty / Density", "slider", 0.005, 0.98, 0.005, reset_value=0.5, tooltip="Controls strobe on-time or sparkle fill density depending on the pattern.", ), "randomness": ParameterSpec( "randomness", "Randomness", "slider", 0.0, 1.5, 0.01, reset_value=0.35, tooltip="Controls variation in patterns that intentionally use randomness.", ), }