Files
LlamaCasty/Models/VerticalCameraLayout.cs
gramps 3a77662a95 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.
2026-10-21 19:23:29 -07:00

86 lines
4.1 KiB
C#
Raw Permalink 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.Windows;
namespace ytLive.Models;
/// <summary>
/// The vertical (9:16) tier's camera rules — TASK 57, the "vv" feature.
///
/// The vertical tier is a centred band crop of the SAME 1920×1080 master the
/// 16:9 tiers use, so a camera placed for 16:9 lands outside the cropped
/// window and vanishes from the vertical output. The vertical tier therefore
/// keeps its OWN placement per scene: a small tile inside the band, snapped to
/// one of six spots — the band splits into a top and a bottom half, each with
/// a left, a centre and a right slot.
///
/// Geometry only: the preview and the compositor both read the element's own
/// X/Y/W/H + ClipShape, so the placement is resolved by swapping those values
/// when the tier changes. No render-path change.
/// </summary>
public static class VerticalCameraLayout
{
/// <summary>The vertical tier's camera tile over the master frame —
/// 270×152 master px = 480×270 in the 1080×1920 output.</summary>
public const double TileWidth = 270;
public const double TileHeight = 152;
/// <summary>How far the tile keeps from the band's outer edge — the app's
/// own placement margin (32 master px) used by the default webcam spot.</summary>
public const double Inset = 32;
/// <summary>True for the portrait tier (the vertical 1080×1920 one).</summary>
public static bool IsVerticalTier(QualityOption quality) => quality.Height > quality.Width;
/// <summary>The half of the band a tile lives in — top or bottom.</summary>
public static Rect AreaFor(Rect band, bool bottom)
=> new(band.Left, bottom ? band.Top + band.Height / 2 : band.Top, band.Width, band.Height / 2);
/// <summary>The largest a camera may be inside the band: one whole area.</summary>
public static (double Width, double Height) MaxSizeFor(Rect band)
=> (band.Width, band.Height / 2);
/// <summary>True when a camera is wide enough to fill one whole area of the
/// band — the "enlarged" Chat layout (one 607×540 area).</summary>
public static bool FillsArea(Rect band, double width) => width >= band.Width - 1;
/// <summary>The two sizes a camera may take in the vertical tier's Chat
/// layout: the small tile (normal) or one whole area (enlarged). Returns
/// the one nearer the requested width, so the resize handle snaps between
/// them instead of taking any size in between.</summary>
public static (double Width, double Height) SizeFor(Rect band, double requestedWidth)
{
var (areaWidth, areaHeight) = MaxSizeFor(band);
return Math.Abs(requestedWidth - TileWidth) <= Math.Abs(requestedWidth - areaWidth)
? (TileWidth, TileHeight)
: (areaWidth, areaHeight);
}
/// <summary>
/// The spot a tile snaps to: the area (top/bottom) and the column
/// (left/centre/right) its centre falls in, then the tile's top-left for
/// that spot — inset from the band's outer edge, and never outside the band
/// (a tile wider than the inset eats it).
/// </summary>
public static (double X, double Y) SpotFor(Rect band, double width, double height, double centreX, double centreY)
{
var bottom = centreY >= band.Top + band.Height / 2;
var area = AreaFor(band, bottom);
var third = area.Width / 3;
double x;
if (centreX < area.Left + third) x = area.Left + Inset;
else if (centreX < area.Left + 2 * third) x = area.Left + (area.Width - width) / 2;
else x = area.Right - Inset - width;
var y = bottom ? area.Bottom - Inset - height : area.Top + Inset;
x = Math.Clamp(x, band.Left, Math.Max(band.Left, band.Right - width));
y = Math.Clamp(y, band.Top, Math.Max(band.Top, band.Bottom - height));
return (x, y);
}
/// <summary>The default tile spot: the bottom-right corner of the band —
/// the same corner the 16:9 default webcam placement uses.</summary>
public static (double X, double Y) DefaultSpot(Rect band, double width, double height)
=> SpotFor(band, width, height, band.Right, band.Bottom);
}