Files
LlamaCasty/Services/Compositor/ScoreboardPresenter.cs
T
gramps c03acd0aa0 fix(scoreboard): the defaults were never applied — a record struct zero-initialises
Creator's defaults: white text, Background ON (black, 20%), speed capped at 200 px/s, text size shown
in real pixels, and "plate" dropped from the label.

The important find is underneath those: `ScoreboardStyle` is a record STRUCT, and `new
ScoreboardStyle()` zero-initialises — it does NOT run the constructor's parameter defaults. So the
shipped strip had null text colour, NO background and zero opacity, whatever the source said. That is
why the Background checkbox and the opacity slider looked like they had no code behind them: the
renderer was faithfully drawing the zeros it was handed.

An explicit parameterless constructor now carries the real defaults, and the renderer test asserts
them (white / on / black / 0.20 / max speed 200) so this class of silent-zero cannot return.

Readouts fixed too: the opacity and text-size setters never raised their own property, so the numbers
never moved while the render did. Text size now reads "24 px" instead of a bare slider value, and
snaps to whole pixels.
2026-10-30 15:24:14 -07:00

96 lines
3.5 KiB
C#

using System.Collections.Generic;
using ytLive.Models;
namespace ytLive.Services.Compositor;
/// <summary>Turns the session's tallies into the strip frame, and keeps the raster until a displayed
/// value actually moves.
///
/// <para>The cache is the point: a strip re-rasterised every tick would be the most expensive layer
/// the compositor carries, for pixels that did not change. `ScoreboardRecorder.Changed` only fires
/// on a visible change, so the presenter rebuilds only then.</para></summary>
public sealed class ScoreboardPresenter
{
private readonly ScoreboardRecorder _recorder;
private VideoFrame? _frame;
private VideoFrame? _run; // the wide scrollable run (dev preview only)
private double _offsetPx;
private string? _tilesFingerprint;
private ScoreboardStyle _style;
private ScoreboardTiles _tiles = ScoreboardTiles.None;
public ScoreboardPresenter(ScoreboardRecorder recorder, ScoreboardStyle style)
{
_recorder = recorder;
_style = style;
_recorder.Changed += Invalidate;
}
/// <summary>The strip TICKS (creator ruling 2026-10-30: "the point is, it's a ticker… so,
/// tick"). True for the product, and the dev simulation uses the same path — so what the
/// creator previews is what ships.</summary>
public bool Scroll { get; set; } = true;
/// <summary>Marquee speed in pixels per second — the creator's dial (a read-speed choice, not a
/// styling one).</summary>
public double PixelsPerSecond { get; set; } = DefaultPixelsPerSecond;
public const double DefaultPixelsPerSecond = 120;
public const double MinPixelsPerSecond = 20;
public const double MaxPixelsPerSecond = 200; // creator cap: faster than this is unreadable
/// <summary>Advances the scroll by <paramref name="seconds"/>. The strip is a marquee: one
/// measured run, cropped to a moving window, so a tick is a row-wise byte copy rather than a
/// re-rasterisation.</summary>
public void Advance(double seconds, double? pixelsPerSecond = null)
{
if (!Scroll) return;
_offsetPx += seconds * (pixelsPerSecond ?? PixelsPerSecond);
}
/// <summary>The strip frame for the current tallies, or null when nothing is enabled/shown.</summary>
public VideoFrame? Frame
{
get
{
var tiles = _tiles.Build(_recorder.State);
var fingerprint = $"{_tiles.CacheKey}|{string.Join('|', tiles)}|{_style}";
if (Scroll)
{
if (_run is null || fingerprint != _tilesFingerprint)
{
_tilesFingerprint = fingerprint;
_run = ScoreboardRenderer.RenderRun(tiles, _style);
}
return _run is null ? null : ScoreboardRenderer.Crop(_run, (int)_offsetPx, ScoreboardRenderer.Width);
}
if (_frame != null && fingerprint == _tilesFingerprint) return _frame;
_tilesFingerprint = fingerprint;
_frame = ScoreboardRenderer.Render(tiles, _style);
return _frame;
}
}
public void SetTiles(ScoreboardTiles tiles)
{
_tiles = tiles;
Invalidate();
}
public void SetStyle(ScoreboardStyle style)
{
_style = style;
Invalidate();
}
/// <summary>Drops the cached raster so the next read rebuilds it.</summary>
public void Invalidate()
{
_frame = null;
_run = null;
_tilesFingerprint = null;
}
}