from __future__ import annotations from app.config.models import SegmentConfig, TileConfig from app.core.types import clamp NormalizedPoint = tuple[float, float] NormalizedInsets = tuple[float, float] _SEGMENT_SIDE_ALIASES = { "left": "left", "right": "right", "top": "top", "bottom": "bottom", "l": "left", "r": "right", "t": "top", "b": "bottom", } def segment_side(tile: TileConfig, segment: SegmentConfig) -> str | None: side = (segment.side or "").strip().lower() if side in _SEGMENT_SIDE_ALIASES: return _SEGMENT_SIDE_ALIASES[side] width = max(0.001, tile.x1 - tile.x0) height = max(0.001, tile.y1 - tile.y0) delta_x = abs(segment.x1 - segment.x0) / width delta_y = abs(segment.y1 - segment.y0) / height mid_x = (segment.x0 + segment.x1) / 2.0 mid_y = (segment.y0 + segment.y1) / 2.0 if delta_x >= delta_y: return "top" if mid_y <= tile.y0 + height * 0.5 else "bottom" return "left" if mid_x <= tile.x0 + width * 0.5 else "right" def segment_led_position( tile: TileConfig, segment: SegmentConfig, amount: float, *, insets: NormalizedInsets = (0.0, 0.0), apply_reverse: bool = False, ) -> NormalizedPoint: amount = clamp(float(amount)) if apply_reverse and segment.reverse: amount = 1.0 - amount inset_x = clamp(float(insets[0]), 0.0, 0.49) inset_y = clamp(float(insets[1]), 0.0, 0.49) side = segment_side(tile, segment) if side == "left": return inset_x, inset_y + (1.0 - inset_y * 2.0) * amount if side == "right": return 1.0 - inset_x, inset_y + (1.0 - inset_y * 2.0) * amount if side == "top": return inset_x + (1.0 - inset_x * 2.0) * amount, inset_y if side == "bottom": return inset_x + (1.0 - inset_x * 2.0) * amount, 1.0 - inset_y width = max(0.001, tile.x1 - tile.x0) height = max(0.001, tile.y1 - tile.y0) x_pos = segment.x0 + (segment.x1 - segment.x0) * amount y_pos = segment.y0 + (segment.y1 - segment.y0) * amount return ( clamp((x_pos - tile.x0) / width), clamp((y_pos - tile.y0) / height), ) def segment_led_positions( tile: TileConfig, segment: SegmentConfig, *, insets: NormalizedInsets = (0.0, 0.0), ) -> list[NormalizedPoint]: count = max(1, segment.led_count) return [ segment_led_position( tile, segment, 0.0 if count == 1 else index / (count - 1), insets=insets, apply_reverse=True, ) for index in range(count) ]