Update docs and screenshots
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
|
||||
|
||||
def clamp_channel(value: int) -> int:
|
||||
return max(0, min(255, int(value)))
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FrameLayer:
|
||||
name: str
|
||||
priority: int
|
||||
values_by_universe: dict[int, dict[int, int]]
|
||||
precedence_map_by_universe: dict[int, dict[int, str]] = field(default_factory=dict)
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
@property
|
||||
def values(self) -> dict[int, int]:
|
||||
return self.values_by_universe.get(1, {})
|
||||
|
||||
@property
|
||||
def precedence_map(self) -> dict[int, str]:
|
||||
return self.precedence_map_by_universe.get(1, {})
|
||||
|
||||
@classmethod
|
||||
def from_channel_values(
|
||||
cls,
|
||||
name: str,
|
||||
priority: int,
|
||||
values: dict[int, int],
|
||||
precedence_map: dict[int, str] | None = None,
|
||||
*,
|
||||
universe: int = 1,
|
||||
) -> "FrameLayer":
|
||||
return cls(
|
||||
name=name,
|
||||
priority=priority,
|
||||
values_by_universe={universe: values},
|
||||
precedence_map_by_universe={universe: precedence_map or {}},
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DmxFrame:
|
||||
universe: int = 1
|
||||
values: list[int] = field(default_factory=lambda: [0] * 512)
|
||||
source_map: list[str] = field(default_factory=lambda: ["idle"] * 512)
|
||||
|
||||
def set_channel(self, channel: int, value: int, source: str = "manual") -> None:
|
||||
if not 1 <= channel <= 512:
|
||||
raise ValueError("Channel must be in range 1..512")
|
||||
self.values[channel - 1] = clamp_channel(value)
|
||||
self.source_map[channel - 1] = source
|
||||
|
||||
def copy(self) -> DmxFrame:
|
||||
return DmxFrame(self.universe, self.values.copy(), self.source_map.copy())
|
||||
|
||||
|
||||
def merge_layers(layers: list[FrameLayer], blackout: bool = False) -> dict[int, DmxFrame]:
|
||||
universes = sorted(
|
||||
{
|
||||
int(universe)
|
||||
for layer in layers
|
||||
for universe in layer.values_by_universe
|
||||
}
|
||||
) or [1]
|
||||
if blackout:
|
||||
return {universe: DmxFrame(universe=universe) for universe in universes}
|
||||
|
||||
ordered = sorted(layers, key=lambda layer: (layer.priority, layer.created_at))
|
||||
frames = {universe: DmxFrame(universe=universe) for universe in universes}
|
||||
for universe, frame in frames.items():
|
||||
for channel in range(1, 513):
|
||||
channel_candidates: list[tuple[FrameLayer, int]] = []
|
||||
for layer in ordered:
|
||||
values = layer.values_by_universe.get(universe, {})
|
||||
if channel in values:
|
||||
channel_candidates.append((layer, clamp_channel(values[channel])))
|
||||
if not channel_candidates:
|
||||
continue
|
||||
|
||||
precedence_map = channel_candidates[-1][0].precedence_map_by_universe.get(universe, {})
|
||||
precedence = precedence_map.get(channel, "ltp").lower()
|
||||
if precedence == "htp":
|
||||
chosen_layer, chosen_value = max(channel_candidates, key=lambda pair: pair[1])
|
||||
else:
|
||||
chosen_layer, chosen_value = channel_candidates[-1]
|
||||
frame.set_channel(channel, chosen_value, chosen_layer.name)
|
||||
return frames
|
||||
Reference in New Issue
Block a user