Creator corrected the "loupe" zoom on 2026-10-08: the click anchors a fixed
capture region (box/2 centred on the click) and the box is a live 2x view of
that region — dragging the box never changes its content; it magnifies the
fully-composited frame (every layer, incl. webcam) and draws LAST (n+1).
Rectangle shape is 4:3 ("more of a 640x480 aspect").
- MainViewModel.Zoom.cs rewritten: ActivateZoom anchors CaptureX/CaptureY
(independently clamped); UpdateZoomMove relocates the box only; the per-frame
BuildZoomFrame/ZoomFrameBuilder path is gone.
- SceneCompositor: skips the active ZoomSceneElement in every layer loop and
BlitZooms it last via the new pure ZoomMagnifier (2x bilinear, temp snapshot
via ArrayPool, round clip).
- Preview: ZoomBox hoisted above CanvasGrid as a sibling; its brush samples the
whole composed preview at the fixed capture (2x).
- Tests: ZoomMagnifierTests (box-over-blue shows green capture at 2x incl. an
extra red "layer" — the loupe/backdrop-only regressions both fail it) +
round-clip; ZoomBoxInteractionTests rewritten (anchored capture, move-doesn't-
change-capture, 4:3/9:16/square). 404 pass, 2 known environment flakes.
118 lines
5.2 KiB
C#
118 lines
5.2 KiB
C#
using System;
|
|
using Xunit;
|
|
using ytLive.Services.Compositor;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// Proves the zoom box's pixels match the creator-corrected model (2026-10-08):
|
|
/// the box is a GENUINE 2x magnified view of a FIXED capture window (the box/2
|
|
/// region centred on the click) sampled from the ALREADY-COMPOSITED frame — i.e.
|
|
/// every layer, and NOT what happens to sit under the box. The money test below
|
|
/// puts the box over the blue half of the frame while its capture sits in the
|
|
/// green half (with a red webcam square + a white 2px stripe inside): a 1x
|
|
/// loupe, a box-following loupe, or a backdrop-only sample all fail hard.
|
|
/// </summary>
|
|
public sealed class ZoomMagnifierTests
|
|
{
|
|
private const int W = 1920;
|
|
private const int H = 1080;
|
|
|
|
/// <summary>1920x1080 opaque "composited scene": blue left half (x<960),
|
|
/// green right half, plus a red 40x40 webcam square and a white 2px stripe
|
|
/// inside the capture region — the extra "layers" the box must capture.</summary>
|
|
private static byte[] ComposedFrame()
|
|
{
|
|
var buf = new byte[W * H * 4];
|
|
for (var y = 0; y < H; y++)
|
|
{
|
|
for (var x = 0; x < W; x++)
|
|
{
|
|
var i = (y * W + x) * 4;
|
|
buf[i] = 0; buf[i + 1] = 0; buf[i + 2] = 0; buf[i + 3] = 255; // black
|
|
if (x >= 960) { buf[i] = 0; buf[i + 1] = 255; buf[i + 2] = 0; } // green
|
|
else { buf[i] = 255; buf[i + 1] = 0; buf[i + 2] = 0; } // blue
|
|
if (x >= 1000 && x < 1040 && y >= 520 && y < 560) { buf[i] = 0; buf[i + 1] = 0; buf[i + 2] = 255; } // red webcam square
|
|
if (x >= 1100 && x < 1102) { buf[i] = 255; buf[i + 1] = 255; buf[i + 2] = 255; } // white stripe
|
|
}
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
private static byte[] Pixel(byte[] buf, int x, int y)
|
|
{
|
|
var i = (y * W + x) * 4;
|
|
return new[] { buf[i], buf[i + 1], buf[i + 2] };
|
|
}
|
|
|
|
private static void AssertColor(byte[] buf, int x, int y, byte b, byte g, byte r)
|
|
{
|
|
var p = Pixel(buf, x, y);
|
|
Assert.Equal(new[] { b, g, r }, p);
|
|
}
|
|
|
|
[Fact]
|
|
public void Magnify_BoxOverBlue_ShowsFixedGreenCapture_WithAllLayers_At2x()
|
|
{
|
|
var buf = ComposedFrame();
|
|
|
|
// Click at (1080, 540). Box 400x200 centred on the click → box [880,1280)x[440,640).
|
|
// Capture = box/2 → [980,1180)x[490,590), entirely in the GREEN half and holding
|
|
// the red webcam square [1000,1040)x[520,560) and the white stripe [1100,1102).
|
|
ZoomMagnifier.MagnifyInto(buf, W, H,
|
|
boxX: 880, boxY: 440, boxW: 400, boxH: 200,
|
|
capX: 980, capY: 490, round: false);
|
|
|
|
// The whole box shows the CAPTURE (green + its layer content), not the frame
|
|
// beneath it — the box's left quarter sits over the BLUE half. Any blue inside
|
|
// the box means the sampler followed the box (the loupe bug). Green/red/white
|
|
// are all fine (edges blend at the bright stripes), blue (B≫G) is the crime.
|
|
for (var y = 440; y < 640; y++)
|
|
{
|
|
for (var x = 880; x < 1280; x++)
|
|
{
|
|
var p = Pixel(buf, x, y);
|
|
Assert.True(p[1] >= p[0],
|
|
$"box pixel ({x},{y}) = ({p[0]},{p[1]},{p[2]}) — blue means the sampler followed the box, never the capture");
|
|
}
|
|
}
|
|
|
|
// The red webcam square is inside the capture → it is captured (all layers!).
|
|
// Capture-local [20,60)x[30,70) maps 2x to box [920,1000)x[500,580).
|
|
for (var x = 925; x < 995; x++)
|
|
for (var y = 505; y < 575; y++)
|
|
AssertColor(buf, x, y, 0, 0, 255);
|
|
AssertColor(buf, 1010, 570, 0, 255, 0); // past the square → green
|
|
|
|
// The 2x proof: the 2px white stripe at capture [1100,1102) must land at box
|
|
// [1120,1121] (offset*2) — a 1x sample would leave it near capture space and a
|
|
// box-following sample would miss it entirely. Probe the interior (solid white)
|
|
// and either side (solid green), skipping bilinear edge blends.
|
|
AssertColor(buf, 1120, 540, 255, 255, 255);
|
|
AssertColor(buf, 1121, 540, 255, 255, 255);
|
|
AssertColor(buf, 1115, 540, 0, 255, 0);
|
|
AssertColor(buf, 1124, 540, 0, 255, 0);
|
|
|
|
// Outside the box the frame is untouched.
|
|
AssertColor(buf, 400, 540, 255, 0, 0); // still blue (left half)
|
|
AssertColor(buf, 1500, 540, 0, 255, 0); // still green
|
|
}
|
|
|
|
[Fact]
|
|
public void Magnify_Round_Clips_To_Circle_Leaving_Corners_Untouched()
|
|
{
|
|
var buf = ComposedFrame();
|
|
|
|
// Same geometry as the money test but circle: radius = min(400,200)/2 = 100.
|
|
ZoomMagnifier.MagnifyInto(buf, W, H,
|
|
boxX: 880, boxY: 440, boxW: 400, boxH: 200,
|
|
capX: 980, capY: 490, round: true);
|
|
|
|
// Box corners are outside the circle: the underlying blue frame shows through.
|
|
AssertColor(buf, 884, 444, 255, 0, 0);
|
|
AssertColor(buf, 884, 636, 255, 0, 0);
|
|
AssertColor(buf, 1276, 444, 0, 255, 0); // right side over green stays green
|
|
// Box centre is inside the circle: captured green.
|
|
AssertColor(buf, 1080, 540, 0, 255, 0);
|
|
}
|
|
} |