From 882b8551d9b2a3a0dacdd4c528bdcd336e999ab3 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 15:11:35 -0700 Subject: [PATCH] fix(scoreboard): the strip and the social bar now really share the frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- Controls/LeftPanel.xaml | 6 ++++ ViewModels/MainViewModel.Scoreboard.cs | 38 +++++++++++++++++------ ViewModels/MainViewModel.Socials.cs | 3 ++ ytLive.Tests/ScoreboardRendererTests.cs | 9 ++---- ytLive.Tests/ScoreboardSimulationTests.cs | 1 - 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml index 1ef1ba3..ca0dd2d 100644 --- a/Controls/LeftPanel.xaml +++ b/Controls/LeftPanel.xaml @@ -473,6 +473,12 @@ + + + Gap between the strip and the frame edge. private const double ScoreboardEdgeMargin = 8; + + private BarEdge _scoreboardPreferredEdge = BarEdge.Bottom; + + /// 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). + public BarEdge ScoreboardEdge + { + get => _scoreboardPreferredEdge; + set + { + if (!SetProperty(ref _scoreboardPreferredEdge, value)) return; + RefreshScoreboardPreviews(); + } + } + + public IReadOnlyList 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; } } diff --git a/ViewModels/MainViewModel.Socials.cs b/ViewModels/MainViewModel.Socials.cs index 5a9955c..ea41b00 100644 --- a/ViewModels/MainViewModel.Socials.cs +++ b/ViewModels/MainViewModel.Socials.cs @@ -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(); } diff --git a/ytLive.Tests/ScoreboardRendererTests.cs b/ytLive.Tests/ScoreboardRendererTests.cs index 9f397bb..a972915 100644 --- a/ytLive.Tests/ScoreboardRendererTests.cs +++ b/ytLive.Tests/ScoreboardRendererTests.cs @@ -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"); diff --git a/ytLive.Tests/ScoreboardSimulationTests.cs b/ytLive.Tests/ScoreboardSimulationTests.cs index c76cae7..6b85044 100644 --- a/ytLive.Tests/ScoreboardSimulationTests.cs +++ b/ytLive.Tests/ScoreboardSimulationTests.cs @@ -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);