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:
2026-10-21 19:23:29 -07:00
parent ccc1db52d3
commit 3a77662a95
17 changed files with 415 additions and 66 deletions
+68 -13
View File
@@ -486,6 +486,11 @@ public partial class MainViewModel
private bool ResolveVerticalCameraLayout(Scene scene, WebcamSceneConfig config, bool vertical)
{
// The Chat scene's vertical layout is RECT-only (the creator's table:
// "vv · Chat = rect only — no circle option"). The lock is cleared
// BEFORE the 16:9 layout comes back, so the desktop shape is restored.
config.VerticalShapeLocked = vertical && SceneCatalog.IsChat(scene.Name);
if (!vertical)
{
config.VerticalBand = Rect.Empty;
@@ -509,12 +514,14 @@ public partial class MainViewModel
return true;
}
// The Live scene's camera must be INSIDE the band or the vertical
// output has no camera at all — it starts as a small tile in the
// bottom-right spot (the corner the 16:9 default uses). The other
// scenes keep their 16:9 placement until the creator drags them into
// the band (creator ruling 2026-10-17: Starting/BRB/Ending unchanged).
if (!SceneCatalog.Is(scene.Name, SceneCatalog.Live)) return false;
// The Live and Chat scenes are the camera scenes in the vertical tier:
// without a placement INSIDE the band the vertical output has no camera
// at all. They start as a small tile in the bottom-right spot (the
// corner the 16:9 default webcam uses). Starting/BRB/Ending keep their
// 16:9 placement (creator ruling 2026-10-17: unchanged) until the
// creator drags them into the band.
if (!SceneCatalog.Is(scene.Name, SceneCatalog.Live) && !SceneCatalog.IsChat(scene.Name))
return false;
var spot = VerticalCameraLayout.DefaultSpot(
VerticalBand, VerticalCameraLayout.TileWidth, VerticalCameraLayout.TileHeight);
@@ -534,11 +541,9 @@ public partial class MainViewModel
{
if (!IsVerticalTier) return;
EnsureVerticalCameraLayout(config);
var spot = VerticalCameraLayout.SpotFor(
VerticalBand, config.Width, config.Height,
x + config.Width / 2, y + config.Height / 2);
config.X = spot.X;
config.Y = spot.Y;
config.X = x;
config.Y = y;
SnapWebcamInVerticalTier(config);
}
/// <summary>Snaps a camera back onto its spot — the resize handle grew or
@@ -547,13 +552,63 @@ public partial class MainViewModel
{
if (!IsVerticalTier) return;
EnsureVerticalCameraLayout(config);
var centreX = config.X + config.Width / 2;
var centreY = config.Y + config.Height / 2;
// The enlarged Chat camera fills one whole area — the top one or the
// bottom one, by where its centre is.
if (VerticalCameraLayout.FillsArea(VerticalBand, config.Width))
{
var area = VerticalCameraLayout.AreaFor(
VerticalBand, centreY >= VerticalBand.Top + VerticalBand.Height / 2);
config.Width = area.Width;
config.Height = area.Height;
config.X = area.Left;
config.Y = area.Top;
return;
}
var spot = VerticalCameraLayout.SpotFor(
VerticalBand, config.Width, config.Height,
config.X + config.Width / 2, config.Y + config.Height / 2);
VerticalBand, config.Width, config.Height, centreX, centreY);
config.X = spot.X;
config.Y = spot.Y;
}
/// <summary>True when this camera belongs to the Chat scene — the vertical
/// tier's Chat layout has its own rules (rect only, two sizes).</summary>
public bool IsChatSceneCamera(WebcamSceneConfig config)
=> Scenes.Any(s => SceneCatalog.IsChat(s.Name) && s.Elements.Contains(config));
/// <summary>False when the circle option is not offered — the Chat scene's
/// vertical layout is rect-only (the creator's table, 2026-10-17).</summary>
public bool CanToggleClipShape => !(IsVerticalTier
&& SelectedElement is WebcamSceneConfig selected
&& IsChatSceneCamera(selected));
/// <summary>Applies a resize inside the vertical tier: the Chat camera's
/// size is discrete — the small tile (normal) or one whole 607×540 area
/// (enlarged) — and every camera ends up snapped to its spot.</summary>
public void ResizeWebcamInVerticalTier(WebcamSceneConfig config, double requestedWidth, double requestedHeight)
{
if (!IsVerticalTier) return;
EnsureVerticalCameraLayout(config);
if (IsChatSceneCamera(config))
{
var (w, h) = VerticalCameraLayout.SizeFor(VerticalBand, requestedWidth);
config.Width = w;
config.Height = h;
}
else
{
config.Width = requestedWidth;
config.Height = requestedHeight;
}
SnapWebcamInVerticalTier(config);
}
/// <summary>Makes the scene's vertical placement live — creating it (a small
/// tile in the default spot) when the scene has none yet.</summary>
private void EnsureVerticalCameraLayout(WebcamSceneConfig config)