Files
LlamaCasty/Models/VerticalCameraLayout.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

70 lines
3.2 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.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>
/// 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);
}