The creator's locked table (2026-10-17) gives the Chat scene its own rules in the 9:16 tier: RECT ONLY - the circle option is not offered - and a size that is either the normal small tile or ENLARGED to fill one whole 607x540 area (top or bottom). Unit 1 shipped the per-scene placement; this unit is the Chat scene's own variant of it. The rule lives on the MODEL, not on the button: - Models/SceneElement.cs: ClipShape and ToggleClipShape() are now virtual. - Models/WebcamSceneConfig.cs: VerticalShapeLocked (set per scene by the VM) OVERRIDES the ClipShape property, so no writer can slip a circle in - the toggle, the Shape button and the placement swap all go through it. The lock is cleared BEFORE the 16:9 layout comes back, so the desktop shape returns. - ViewModels/MainViewModel.Webcam.cs: CanToggleClipShape (the circle is NOT offered in the vertical Chat layout - the Shape button disappears, it is not merely greyed out), IsChatSceneCamera, ResizeWebcamInVerticalTier. - Models/VerticalCameraLayout.cs: FillsArea (the "enlarged" state, derived - no new column) and SizeFor - the Chat camera's TWO sizes, the tile or the whole area, whichever is nearer the requested width, so the resize handle has two detents instead of any size in between. - An enlarged camera FILLS the area its centre is in (top or bottom), so dragging it up/down swaps the areas. - The Chat camera is auto-tiled like the Live one when the vertical tier is picked - otherwise the vertical chat stream has no camera at all. - Controls/PreviewPane.xaml.cs: the resize routes through the VM; the drag assigns, lets the model clamp into the band, then snaps (so the enlarged camera can be dragged from the top area to the bottom one). Test: ytLive.Tests/VerticalChatCameraLayoutTests.cs - the ONE Good Dog test for the unit: the vertical tier is picked -> the Chat camera comes into the band as a small RECT tile -> the circle option is not offered and cannot be set even on the model -> dragged up and enlarged it fills the TOP area exactly -> back to 16:9 the desktop layout is restored exactly and the circle option is offered again. 430/430 tests pass, 0 warnings. Research (the creator's table is the established practice, not our invention): - Streamlabs' vertical guide: "splitting the screen in half horizontally, placing the camera feed in the top half and the main content on the bottom (or vice versa) may be the bet", and a just-chatting vertical wants the camera "to take up most of the screen" (streamlabs.com/content-hub/post/overlays-vertical-and-horizontal-streaming) - Bloopbot's vertical overlay docs describe the same per-layer model we use at scene granularity: "move or resize a layer on Vertical and it stops being Auto-placed. It keeps the place you gave it, and the horizontal layout is not touched" (bloopbot.com/docs/vertical-overlays-twitch-dual-format) - our swap-at-tier-change, stored per scene. - Landmine recorded: "about the bottom 560 px of the vertical video can be covered by Twitch's chat and player controls on phones" (Bloopbot) - the Chat camera's enlarged BOTTOM area may be half-hidden by the platform chrome; TASK 53's safe-zone work decides that. Memory in the same commit: ai.md (the Chat rules, the shape lock, the tests), TASKS.md + TASKS/task-57-vertical-camera-layout.md (TASK 57 complete), HANDOFF.md, MyMistakes.md (3 recipes: lock the model property not the UI button; two detents for a discrete size set; the edit tool cannot match an oldString with an em dash), Models/ViewModels/Controls index rows.
115 lines
4.7 KiB
C#
115 lines
4.7 KiB
C#
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 2 — the vertical (9:16) tier's CHAT scene
|
||
/// camera.
|
||
///
|
||
/// Unit 1 gave every scene its own vertical placement. Unit 2 is the Chat
|
||
/// scene's own rules (the creator's table, 2026-10-17): in the vertical tier
|
||
/// the Chat camera is **rect only — the circle option is not offered** — and
|
||
/// its size is either the normal small tile or **enlarged to fill one whole
|
||
/// 607×540 area** (the top one or the bottom one, by where the camera is).
|
||
///
|
||
/// One test, the whole story (the Good Dog rule): the vertical tier is picked →
|
||
/// the Chat camera comes into the band as a small RECT tile → the circle option
|
||
/// is not offered and cannot be set even by the model → the camera is dragged up
|
||
/// and enlarged, and it fills the TOP area exactly → switching back to 16:9
|
||
/// restores the desktop layout exactly (and the circle option is offered again).
|
||
/// </summary>
|
||
[Collection("RealApp")]
|
||
public sealed class VerticalChatCameraLayoutTests
|
||
{
|
||
private readonly RealAppHost _app;
|
||
|
||
public VerticalChatCameraLayoutTests(RealAppHost app) => _app = app;
|
||
|
||
[Fact]
|
||
public void Vertical_Tier_Chat_Camera_Is_Rect_Only_And_Enlarges_To_One_Area()
|
||
{
|
||
_app.Run(Run);
|
||
}
|
||
|
||
private void Run()
|
||
{
|
||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-vv-chat-{Guid.NewGuid():N}.db");
|
||
MainViewModel.LayoutPathOverride = tempDb;
|
||
try
|
||
{
|
||
var vm = new MainViewModel();
|
||
|
||
var chat = vm.Scenes.First(s => s.Name == "Chat");
|
||
var camera = new WebcamSceneConfig
|
||
{
|
||
WebcamId = "webcam-1",
|
||
Name = "Webcam",
|
||
X = 120,
|
||
Y = 120,
|
||
Width = 640,
|
||
Height = 360,
|
||
};
|
||
chat.Elements.Add(camera);
|
||
|
||
// The 16:9 placement: the camera sits where the desktop layout put it.
|
||
Assert.Equal(120, camera.X);
|
||
Assert.Equal(120, camera.Y);
|
||
Assert.Equal(640, camera.Width);
|
||
|
||
// ── The vertical tier is picked ───────────────────────────────────
|
||
vm.SelectedQuality = vm.QualityOptions[^1];
|
||
Assert.True(vm.IsVerticalTier);
|
||
var band = vm.VerticalBand;
|
||
|
||
// The Chat camera came INTO the band as a small tile...
|
||
Assert.Equal(VerticalCameraLayout.TileWidth, camera.Width);
|
||
Assert.Equal(VerticalCameraLayout.TileHeight, camera.Height);
|
||
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");
|
||
|
||
// ── The circle option is not offered ─────────────────────────────
|
||
vm.SelectedElement = camera;
|
||
Assert.False(vm.CanToggleClipShape);
|
||
Assert.Equal(ClipShape.Traditional, camera.ClipShape);
|
||
|
||
// ...and it cannot be slipped in behind the UI's back either.
|
||
camera.ToggleClipShape();
|
||
Assert.Equal(ClipShape.Traditional, camera.ClipShape);
|
||
|
||
// ── Drag it up, then enlarge it: it fills the TOP area ──────────
|
||
vm.MoveWebcamInVerticalTier(camera, band.Left + 40, band.Top + 40);
|
||
vm.ResizeWebcamInVerticalTier(camera, band.Width, band.Height);
|
||
|
||
Assert.Equal(band.Left, camera.X, 3);
|
||
Assert.Equal(band.Top, camera.Y, 3);
|
||
Assert.Equal(band.Width, camera.Width, 3);
|
||
Assert.Equal(band.Height / 2, camera.Height, 3);
|
||
|
||
// ── Back to 16:9: the desktop layout is restored exactly ─────────
|
||
vm.SelectedQuality = vm.QualityOptions[0];
|
||
Assert.False(vm.IsVerticalTier);
|
||
Assert.Equal(120, camera.X);
|
||
Assert.Equal(120, camera.Y);
|
||
Assert.Equal(640, camera.Width);
|
||
Assert.Equal(360, camera.Height);
|
||
|
||
// The circle option is offered again outside the vertical tier.
|
||
Assert.True(vm.CanToggleClipShape);
|
||
}
|
||
finally
|
||
{
|
||
MainViewModel.LayoutPathOverride = null;
|
||
SqliteConnection.ClearAllPools();
|
||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||
}
|
||
}
|
||
}
|