from __future__ import annotations from app.core.types import PatternParameters, clamp SCAN_ANGLES: tuple[int, ...] = (0, 45, 90, 135, 180, 225, 270, 315) _LEGACY_SCAN_IDS = { "row_chase", "row_scan", "column_chase", "column_scan", "diagonal_scan", } _DEFAULT_SCAN_PARAMS = PatternParameters() def nearest_scan_angle(value: float) -> int: angle = int(round(float(value))) % 360 return min(SCAN_ANGLES, key=lambda candidate: min((candidate - angle) % 360, (angle - candidate) % 360)) def coerce_bool(value: object, default: bool = False) -> bool: if isinstance(value, bool): return value if isinstance(value, (int, float)): return bool(value) if isinstance(value, str): normalized = value.strip().lower() if normalized in {"1", "true", "yes", "on"}: return True if normalized in {"0", "false", "no", "off"}: return False return default def _flip_angle_horizontal(angle: int) -> int: return (180 - angle) % 360 def _flip_angle_vertical(angle: int) -> int: return (-angle) % 360 def normalize_pattern_request( pattern_id: str, params: PatternParameters | None = None, rows: int | None = None, cols: int | None = None, ) -> tuple[str, PatternParameters | None]: if pattern_id == "pixel_sparkle": if params is None: return "strobe", None payload = params.to_dict() payload["strobe_mode"] = "random_pixels" return "strobe", PatternParameters.from_dict(payload) if pattern_id == "random_blocks": return "sparkle", params if pattern_id == "scan" and params is not None: payload = params.to_dict() angle = nearest_scan_angle(getattr(params, "angle", payload.get("angle", 0.0))) if coerce_bool(getattr(params, "flip_horizontal", False)): angle = _flip_angle_horizontal(angle) if coerce_bool(getattr(params, "flip_vertical", False)): angle = _flip_angle_vertical(angle) payload["angle"] = angle payload.pop("band_thickness", None) payload.pop("flip_horizontal", None) payload.pop("flip_vertical", None) return "scan", PatternParameters.from_dict(payload) if pattern_id not in _LEGACY_SCAN_IDS: return pattern_id, params payload = params.to_dict() if params is not None else {} if params is not None: payload["flip_horizontal"] = getattr(params, "flip_horizontal", payload.get("flip_horizontal", False)) payload["flip_vertical"] = getattr(params, "flip_vertical", payload.get("flip_vertical", False)) payload["band_thickness"] = getattr(params, "band_thickness", payload.get("band_thickness", params.on_width)) direction = str(payload.get("direction", "left_to_right")) diagonal_mode = str(payload.get("diagonal_scan_mode", "line")) def preferred_value(key: str, legacy_value): current_value = payload.get(key, getattr(_DEFAULT_SCAN_PARAMS, key)) default_value = getattr(_DEFAULT_SCAN_PARAMS, key) return legacy_value if current_value == default_value else current_value if pattern_id in {"row_chase", "row_scan"}: angle = 90 if direction != "bottom_to_top" else 270 on_width = float(preferred_value("on_width", payload.get("block_size", 1.0))) off_width = float(preferred_value("off_width", 0.0)) scan_style = str(preferred_value("scan_style", "line")) elif pattern_id in {"column_chase", "column_scan"}: angle = 0 if direction != "right_to_left" else 180 on_width = float(preferred_value("on_width", payload.get("block_size", 1.0))) off_width = float(preferred_value("off_width", 0.0)) scan_style = str(preferred_value("scan_style", "line")) else: angle = { "left_to_right": 315, "right_to_left": 135, "top_to_bottom": 315, "bottom_to_top": 135, }.get(direction, 315) scan_style = "bands" if diagonal_mode == "bands" else "line" scan_style = str(preferred_value("scan_style", scan_style)) on_width = float(preferred_value("on_width", 2.0 if scan_style == "bands" else 0.5)) off_width = float(preferred_value("off_width", 1.5 if scan_style == "bands" else 0.0)) angle = nearest_scan_angle(preferred_value("angle", angle)) if coerce_bool(preferred_value("flip_horizontal", False)): angle = _flip_angle_horizontal(angle) if coerce_bool(preferred_value("flip_vertical", False)): angle = _flip_angle_vertical(angle) payload["angle"] = angle payload["scan_style"] = str(scan_style) payload["on_width"] = clamp(float(on_width), 0.1, 2.0) payload["off_width"] = clamp(float(off_width), 0.0, 2.0) payload.pop("band_thickness", None) payload.pop("flip_horizontal", None) payload.pop("flip_vertical", None) return "scan", PatternParameters.from_dict(payload)