Files
LlamaCasty/ytLive.Tests/VerticalWebcamLayoutTests.cs
T
gramps 6be062c45f vv webcam Unit 1: the vertical (9:16) tier keeps its OWN camera placement per scene
The vertical tier is a centred band crop of the SAME 1920x1080 master (band x
656.25, w 607.5, h 1080), so the 16:9 camera placement (x~1408) falls outside the
cropped window and the creator is simply not in the vertical stream. Each scene
now keeps its own vertical position, size and shape; switching back to 16:9
restores the desktop layout exactly (the creator's locked table, 2026-10-17).

Geometry only - NO render-path change. The preview and the compositor both read
the element's own X/Y/W/H + ClipShape, so the tier switch swaps the LIVE geometry:
- Models/VerticalCameraLayout.cs (new): the pure math - the band, its two 607x540
  areas, the six spots (top/bottom x left/centre/right), the 32px inset and the
  270x152 default tile (480x270 in the 1080x1920 output) in the bottom-right spot,
  the corner the 16:9 default webcam uses.
- Models/WebcamSceneConfig.cs: the vertical placement lives in seven additive
  columns (VerticalX/Y/Width/Height/ClipShape/RectWidth/RectHeight; NULL = "this
  scene has no vertical placement yet"). The 16:9 layout stays in the old columns
  and is parked in the transient Landscape* fields while the vertical tier is
  live; Begin/EndVerticalLayout + CommitLiveLayoutToVertical do the swap. The
  X/Y/W/H overrides clamp every write into the band - outside it the camera is
  invisible in the vertical output.
- Models/SceneElement.cs: X/Y/Width/Height became virtual so the derived element
  constrains every writer (drag, resize handle, typed fields) in ONE choke point
  instead of three writers each re-implementing the rule.
- ViewModels/MainViewModel.Webcam.cs + .Streaming.cs: IsVerticalTier/VerticalBand,
  ResolveVerticalCameraLayout(s) (the tier swap), MoveWebcamInVerticalTier /
  SnapWebcamInVerticalTier (the six-spot snap, by the tile's centre),
  EnsureVerticalCameraLayout; ApplyStreamQuality is the tier-change hook. The
  Live scene gets its small tile automatically on the first vertical switch (the
  vertical stream would otherwise have no camera at all); the other scenes keep
  their 16:9 placement until the creator drags them into the band.
- Controls/PreviewPane.xaml.cs: the drag/resize route through the VM snap under
  the vertical tier instead of free-floating over the master.
  Controls/LeftPanel.xaml: the webcam properties gained a Geometry section
  (X/Y/W/H) for the manual entry the creator asked for, bound to the LIVE
  placement, gated on the VM's IsWebcamSelected (NOT SelectedElement.IsWebcam -
  the TASK 51 test finds that panel by binding text and would match twice).
- The store: seven guarded ADD COLUMNs, the INSERT/SELECT extended, and the
  X/Y/W/H/ClipShape columns stay the 16:9 layout - while the vertical tier is
  live the save writes the parked values there and the live ones into Vertical*.

Test: ytLive.Tests/VerticalWebcamLayoutTests.cs - the ONE Good Dog test. The
vertical tier brings the Live camera INTO the band as a small tile; dragging it to
the top-right lands it top-right inside the band; back to 16:9 the desktop layout
is restored exactly (1408/778/480x270); both placements survive a save + reload.
Full suite 429/429, 0 warnings. LayerReorderPersistenceTests.RealMouseDrag fails
ALONE on a clean tree in this environment - pre-existing flake, proven by stashing
this work, not a regression.

Derivative work: OBS has no built-in per-aspect-ratio layout storage - the
maintainers' answer is "create a second scene and add the scene you want to send
as source" (obsproject.com/forum/threads/161307) - and Streamyard's dual-aspect
guide says the same ("no built-in way to store separate layouts per aspect ratio",
so you rely on manual workarounds and plugins):
https://streamyard.com/blog/dual-aspect-ratio-streaming-tool
Our per-scene, per-tier stored placement, resolved on the tier change, is the
step past both.

Memory in the same commit: ai.md (new section), TASKS.md + TASKS/task-57-*.md
(Unit 1 shipped, Unit 2 = the Chat scene still open), HANDOFF.md, MyMistakes.md
(the per-tier geometry-swap recipe, the binding-text panel trap, the store's
webcam-identity rule) and the Models/Services/ViewModels/Controls index rows.
2026-10-21 08:58:24 -07:00

135 lines
5.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Linq;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Models;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// TASK 57, the "vv" feature, Unit 1 — the vertical (9:16) tier's camera
/// placement.
///
/// The vertical tier is a centred band crop of the SAME 1920×1080 master, so a
/// camera placed for 16:9 lands outside the cropped window and is simply not
/// in the vertical stream (the Live scene's camera sat at x≈1408 — off the
/// 607.5-wide band entirely). Each scene therefore keeps its OWN vertical
/// placement, and the tier switch swaps the live geometry.
///
/// One test, the whole story (the Good Dog rule): the vertical tier is picked →
/// the Live camera comes into the band as a small tile → dragging it to the
/// top-right snaps it top-right INSIDE the band → switching back to 16:9
/// restores the desktop layout exactly → and the two placements survive a save
/// and reload.
/// </summary>
[Collection("RealApp")]
public sealed class VerticalWebcamLayoutTests
{
private readonly RealAppHost _app;
public VerticalWebcamLayoutTests(RealAppHost app) => _app = app;
[Fact]
public void Vertical_Tier_Puts_The_Live_Camera_In_The_Band_And_16x9_Restores_The_Landscape_Layout()
{
_app.Run(Run);
}
private void Run()
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-vv-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
try
{
var vm = new MainViewModel();
var live = vm.Scenes.First(s => s.Name == "Live");
var camera = new WebcamSceneConfig
{
WebcamId = "webcam-1",
Name = "Webcam",
X = 1408,
Y = 778,
Width = 480,
Height = 270,
};
live.Elements.Add(camera);
// The 16:9 placement: the camera sits where the desktop layout put it.
Assert.Equal(1408, camera.X);
Assert.Equal(778, camera.Y);
Assert.Equal(480, camera.Width);
// ── The vertical tier is picked ───────────────────────────────────
vm.SelectedQuality = vm.QualityOptions[^1];
Assert.True(vm.IsVerticalTier);
var band = vm.VerticalBand;
// The camera came INTO the band (it used to sit outside it entirely).
Assert.True(camera.X >= band.Left, $"camera X {camera.X} is left of the band {band.Left}");
Assert.True(camera.X + camera.Width <= band.Right, "the camera hangs over the band's right edge");
Assert.True(camera.Y >= band.Top, "the camera hangs over the band's top edge");
Assert.True(camera.Y + camera.Height <= band.Bottom, "the camera hangs over the band's bottom edge");
Assert.Equal(VerticalCameraLayout.TileWidth, camera.Width);
Assert.Equal(VerticalCameraLayout.TileHeight, camera.Height);
// ── Drag it to the top-right ─────────────────────────────────────
vm.MoveWebcamInVerticalTier(camera, band.Right - 40, band.Top + 20);
Assert.Equal(band.Right - VerticalCameraLayout.Inset - camera.Width, camera.X, 3);
Assert.Equal(band.Top + VerticalCameraLayout.Inset, camera.Y, 3);
Assert.True(camera.X >= band.Left && camera.X + camera.Width <= band.Right);
Assert.True(camera.Y >= band.Top && camera.Y + camera.Height <= band.Bottom);
var verticalX = camera.X;
var verticalY = camera.Y;
// ── Back to 16:9: the desktop layout is restored exactly ─────────
vm.SelectedQuality = vm.QualityOptions[0];
Assert.False(vm.IsVerticalTier);
Assert.Equal(1408, camera.X);
Assert.Equal(778, camera.Y);
Assert.Equal(480, camera.Width);
Assert.Equal(270, camera.Height);
// ── And the vertical tier comes back with the creator's placement ─
vm.SelectedQuality = vm.QualityOptions[^1];
Assert.Equal(verticalX, camera.X, 3);
Assert.Equal(verticalY, camera.Y, 3);
// ── It survives a save + reload: both placements, side by side ───
// The store writes the webcam rows only when the app has a webcam
// identity (the config rows hang off it) — this box has no camera
// plugged in, so the identity is supplied directly.
using (var store = new Services.LayoutStore(tempDb))
{
store.Save(vm.Scenes, new Webcam { Id = "webcam-1", DeviceId = "device-1", Name = "Webcam" }, null);
}
using var reloaded = new Services.LayoutStore(tempDb);
var reloadedCamera = reloaded.Load().First(s => s.Name == "Live").WebcamConfig;
Assert.NotNull(reloadedCamera);
// The 16:9 layout is the one in the live columns…
Assert.Equal(1408, reloadedCamera.X);
Assert.Equal(778, reloadedCamera.Y);
Assert.Equal(480, reloadedCamera.Width);
Assert.Equal(270, reloadedCamera.Height);
// …and the vertical placement is the creator's own, stored beside it.
Assert.Equal(verticalX, reloadedCamera.VerticalX!.Value, 3);
Assert.Equal(verticalY, reloadedCamera.VerticalY!.Value, 3);
Assert.Equal(VerticalCameraLayout.TileWidth, reloadedCamera.VerticalWidth!.Value, 3);
Assert.Equal(VerticalCameraLayout.TileHeight, reloadedCamera.VerticalHeight!.Value, 3);
}
finally
{
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
}