fix(scoreboard): the strip and the social bar now really share the frame
Reported: the ticker was painting OVER the socials. The coupling rule existed and was tested (ScoreboardLayout.ResolveEdges) but nothing ever CALLED it — the strip's Y still came from its own preference, so both bars landed on the south edge. Now the resolution runs at publish time: the social bar keeps its side, the scoreboard takes the other one, and its Y follows from that edge. The pair re-sorts whenever the social bar moves (the preview click-toggle) or shows/hides, because the rule is "resolve from what is visible", not a stored pairing. The layer also gains an Edge preference (Top/Bottom) in its properties, labelled "socials take the other" — a preference, never a claim on an occupied edge. Also stabilised the opacity assertion: it samples a plate-only pixel 5px from the left edge instead of the first semi-transparent pixel it finds (which could be anti-aliased text).
This commit is contained in:
@@ -473,6 +473,12 @@
|
||||
<CheckBox Style="{StaticResource YtCheckBox}" Margin="0,0,0,4"
|
||||
Content="Show the scoreboard" IsChecked="{Binding ScoreboardEnabled, Mode=TwoWay}"
|
||||
ToolTip="Off hides the strip in every scene AND stops the channel polling."/>
|
||||
<TextBlock Text="Edge (socials take the other)" Foreground="{StaticResource Brush.OnSurfaceVariant}"
|
||||
FontSize="11" Margin="10,0,0,2"/>
|
||||
<ComboBox Style="{StaticResource YtComboBox}" Height="24" FontSize="11" Margin="10,0,0,6"
|
||||
ItemsSource="{Binding ScoreboardEdgeOptions}"
|
||||
SelectedItem="{Binding ScoreboardEdge, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Text="Facts" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||
<WrapPanel Margin="10,0,0,0">
|
||||
<CheckBox Style="{StaticResource YtCheckBox}" Content="Subscribers" Margin="0,0,10,4"
|
||||
|
||||
@@ -27,6 +27,22 @@ public partial class MainViewModel
|
||||
|
||||
/// <summary>Gap between the strip and the frame edge.</summary>
|
||||
private const double ScoreboardEdgeMargin = 8;
|
||||
|
||||
private BarEdge _scoreboardPreferredEdge = BarEdge.Bottom;
|
||||
|
||||
/// <summary>The edge the creator asked for. It is only a PREFERENCE: the resolver gives the
|
||||
/// scoreboard the edge the social bar does not hold (creator ruling, resolved at render time).</summary>
|
||||
public BarEdge ScoreboardEdge
|
||||
{
|
||||
get => _scoreboardPreferredEdge;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _scoreboardPreferredEdge, value)) return;
|
||||
RefreshScoreboardPreviews();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<BarEdge> ScoreboardEdgeOptions { get; } = new[] { BarEdge.Top, BarEdge.Bottom };
|
||||
private Source? _scoreboardPreviewSource;
|
||||
|
||||
internal ScoreboardRecorder ScoreboardRecorder => _scoreboard;
|
||||
@@ -219,16 +235,18 @@ public partial class MainViewModel
|
||||
frame.BgraPixels, frame.Width * 4, 0);
|
||||
source.VideoImageSource = _scoreboardPreview;
|
||||
|
||||
// The strip's height follows what was RENDERED (one line or two). A fixed 56px box
|
||||
// squashed a wrapped strip instead of showing it, and its Y follows from the edge.
|
||||
if (Math.Abs(source.Height - frame.Height) > 0.5)
|
||||
{
|
||||
source.Width = frame.Width;
|
||||
source.Height = frame.Height;
|
||||
source.Y = _scoreboardStyle is var _ && source.Y > 540
|
||||
? 1080 - frame.Height - ScoreboardEdgeMargin
|
||||
: ScoreboardEdgeMargin;
|
||||
}
|
||||
// The strip's box follows what was RENDERED (one or two lines), and its edge comes
|
||||
// from the SHARED rule: the social bar keeps its side, the scoreboard takes the other.
|
||||
var socialEdge = _socials?.BarPosition == SocialBarPosition.Top ? BarEdge.Top : BarEdge.Bottom;
|
||||
var (_, resolved) = ScoreboardLayout.ResolveEdges(
|
||||
socialVisible: SocialBarVisible, socialPreferred: socialEdge,
|
||||
scoreboardVisible: true, scoreboardPreferred: _scoreboardPreferredEdge);
|
||||
|
||||
source.Width = frame.Width;
|
||||
source.Height = frame.Height;
|
||||
source.Y = resolved == BarEdge.Top
|
||||
? ScoreboardEdgeMargin
|
||||
: 1080 - frame.Height - ScoreboardEdgeMargin;
|
||||
_scoreboardPreviewSource = source;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@ public partial class MainViewModel
|
||||
private void NotifySocialsChanged()
|
||||
{
|
||||
RenderSocialBarFrame();
|
||||
RefreshScoreboardPreviews(); // the pair re-sorts when the social bar shows/hides
|
||||
OnPropertyChanged(nameof(SocialBarTextBrush));
|
||||
OnPropertyChanged(nameof(SocialBarBackgroundBrush));
|
||||
OnPropertyChanged(nameof(HasSocials));
|
||||
@@ -238,6 +239,8 @@ public partial class MainViewModel
|
||||
if (_socials == null || _socials.BarPosition == position) return;
|
||||
_socials.BarPosition = position;
|
||||
OnPropertyChanged(nameof(SocialBarTop));
|
||||
// Coupled pair: moving the social bar moves the scoreboard to the other edge.
|
||||
RefreshScoreboardPreviews();
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
|
||||
@@ -55,12 +55,9 @@ public class ScoreboardRendererTests
|
||||
// Opacity: 20% and 80% must produce visibly different plate transparency.
|
||||
var faint = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", true, "#0000FF", 0.2));
|
||||
var solid = ScoreboardRenderer.Render(Tiles, new ScoreboardStyle("#FFFFFF", true, "#0000FF", 0.8));
|
||||
int AlphaOf(VideoFrame f)
|
||||
{
|
||||
for (var i = 3; i < f.BgraPixels.Length; i += 4)
|
||||
if (f.BgraPixels[i] is > 10 and < 245) return f.BgraPixels[i];
|
||||
return 0;
|
||||
}
|
||||
// Sample a plate-only pixel (5px in from the left edge, vertical middle): the text is
|
||||
// centred, so nothing anti-aliased can pollute the alpha reading.
|
||||
static int AlphaOf(VideoFrame f) => f.BgraPixels[((f.Height / 2) * f.Width + 5) * 4 + 3];
|
||||
Assert.InRange(AlphaOf(faint!), 35, 70); // ~20% of 255
|
||||
Assert.InRange(AlphaOf(solid!), 190, 220); // ~80% of 255
|
||||
Assert.True(AlphaOf(solid!) > AlphaOf(faint!) + 100, "the opacity slider must move the alpha");
|
||||
|
||||
@@ -43,7 +43,6 @@ public class ScoreboardSimulationTests
|
||||
Assert.NotNull(type.GetProperty("ScoreboardSimulate"));
|
||||
Assert.NotNull(type.GetProperty("IsDevelopmentBuild"));
|
||||
// DEBUG-gated at compile time, so a Release build has no dev control at all.
|
||||
var vmSource = SourceAssert.Text("ViewModels", "MainViewModel.Scoreboard.cs");
|
||||
Assert.Contains("#if DEBUG", vmSource);
|
||||
Assert.Contains("private const bool DevelopmentBuild = true;", vmSource);
|
||||
Assert.Contains("private const bool DevelopmentBuild = false;", vmSource);
|
||||
|
||||
Reference in New Issue
Block a user