vv webcam Unit 2: the Chat scene in the vertical tier - rect only, tile or one whole 607x540 area
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.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user