using System.Windows;
using System.Windows.Media;
namespace ytLive.Models;
///
/// A scene's usage of the app-wide webcam — conceptually `webcam.{scene}.config`.
/// The identity lives once on the entity; this object is that
/// scene's placement, clip/mirror, border, and visibility for it. Exists only in
/// scenes where the creator added the webcam.
///
public class WebcamSceneConfig : SceneElement
{
public string WebcamId { get; init; } = string.Empty;
public override bool IsWebcam => true;
public override bool IsDraggable => true;
public override ElementKind Kind => ElementKind.Dynamic;
public override ImageSource? DisplaySource => VideoImageSource;
private BackgroundEffect _backgroundEffect = BackgroundEffect.None;
/// TASK 51: this scene's background effect for the webcam (persisted).
public BackgroundEffect BackgroundEffect
{
get => _backgroundEffect;
set
{
if (!Set(ref _backgroundEffect, value)) return;
Raise(nameof(RemoveBackground));
Raise(nameof(BlurBackground));
Raise(nameof(HasBackgroundEffect));
}
}
/// True while Remove or Blur is on — shows the strength slider.
public bool HasBackgroundEffect => BackgroundEffect != BackgroundEffect.None;
private double _backgroundStrength = 0.5;
/// TASK 51: 0..1 — Blur amount, or Remove's edge softness (persisted).
public double BackgroundStrength
{
get => _backgroundStrength;
set => Set(ref _backgroundStrength, Math.Clamp(value, 0, 1));
}
/// Pill-switch surface: Remove and Blur behave like radio buttons — turning one
/// on turns the other off; turning the active one off means .
public bool RemoveBackground
{
get => BackgroundEffect == BackgroundEffect.Remove;
set => BackgroundEffect = value ? BackgroundEffect.Remove
: BackgroundEffect == BackgroundEffect.Remove ? BackgroundEffect.None : BackgroundEffect;
}
///
public bool BlurBackground
{
get => BackgroundEffect == BackgroundEffect.Blur;
set => BackgroundEffect = value ? BackgroundEffect.Blur
: BackgroundEffect == BackgroundEffect.Blur ? BackgroundEffect.None : BackgroundEffect;
}
// ─── Vertical (9:16) tier placement (TASK 57, the "vv" feature) ────────
// The vertical tier is a centred band crop of the same master frame, so the
// 16:9 placement lands outside the cropped window and the camera vanishes
// from the vertical output. The vertical tier therefore keeps its OWN
// position, size and shape per scene, stored here and resolved when the
// tier changes; switching back to 16:9 restores the desktop layout exactly.
/// The vertical placement's X (null until the scene has one).
public double? VerticalX { get; set; }
/// The vertical placement's Y (null until the scene has one).
public double? VerticalY { get; set; }
/// The vertical placement's width (null until the scene has one).
public double? VerticalWidth { get; set; }
/// The vertical placement's height (null until the scene has one).
public double? VerticalHeight { get; set; }
/// The vertical placement's clip shape (circle or rect) — the
/// creator's own choice per scene, independent of the 16:9 shape.
public ClipShape? VerticalClipShape { get; set; }
/// The vertical placement's pre-Round rect (so a Round tile
/// restores its aspect on toggle-back, like the 16:9 one does).
public double? VerticalRectWidth { get; set; }
///
public double? VerticalRectHeight { get; set; }
/// True once this scene has a vertical placement of its own.
public bool HasVerticalLayout => VerticalX is not null;
/// True while the vertical placement is the LIVE geometry (the
/// element's own X/Y/W/H). The 16:9 layout is parked in
/// and friends until the tier switches back.
public bool VerticalLayoutLive { get; private set; }
/// True while the vertical placement must stay RECT — the Chat
/// scene's vertical layout (the creator's table: "vv · Chat = rect only, no
/// circle option"). The circle is not offered there, so the shape cannot
/// become Round by any writer.
public bool VerticalShapeLocked { get; internal set; }
/// The vertical tier's band over the master frame (transient — the
/// VM sets it when the tier changes). While the vertical placement is live
/// the camera's geometry is clamped to it: outside the band is invisible.
public Rect VerticalBand { get; set; } = Rect.Empty;
// The parked 16:9 layout — meaningful only while the vertical placement is
// live (it is the 16:9 geometry the tier switch restores).
public double LandscapeX { get; private set; }
public double LandscapeY { get; private set; }
public double LandscapeWidth { get; private set; }
public double LandscapeHeight { get; private set; }
public ClipShape LandscapeClipShape { get; private set; }
public double? LandscapeRectWidth { get; private set; }
public double? LandscapeRectHeight { get; private set; }
/// Parks the live (16:9) layout and brings a vertical placement
/// in. The vertical placement becomes the live geometry from here on.
public void BeginVerticalLayout(double x, double y, double width, double height, ClipShape clipShape)
{
LandscapeX = X;
LandscapeY = Y;
LandscapeWidth = Width;
LandscapeHeight = Height;
LandscapeClipShape = ClipShape;
LandscapeRectWidth = RectWidth;
LandscapeRectHeight = RectHeight;
VerticalLayoutLive = true;
Width = width;
Height = height;
X = x;
Y = y;
ClipShape = clipShape;
RectWidth = VerticalRectWidth;
RectHeight = VerticalRectHeight;
}
/// Restores the parked 16:9 layout — the tier switched back.
public void EndVerticalLayout()
{
if (!VerticalLayoutLive) return;
// The vertical placement is the creator's own — keep it, so switching
// back and forth does not lose where they put it.
CommitLiveLayoutToVertical();
VerticalLayoutLive = false;
Width = LandscapeWidth;
Height = LandscapeHeight;
X = LandscapeX;
Y = LandscapeY;
ClipShape = LandscapeClipShape;
RectWidth = LandscapeRectWidth;
RectHeight = LandscapeRectHeight;
}
/// Copies the live geometry into the stored vertical placement, so
/// the stored copy never lags the live one (the save path calls this).
public void CommitLiveLayoutToVertical()
{
VerticalX = X;
VerticalY = Y;
VerticalWidth = Width;
VerticalHeight = Height;
VerticalClipShape = ClipShape;
VerticalRectWidth = RectWidth;
VerticalRectHeight = RectHeight;
}
// The Chat scene's vertical layout is rect-only (the creator's table, the
// circle option is not offered there). Locking the SHAPE property — not
// just the toggle — means no writer can slip a Round camera in: the
// toggle, the shape button and the placement swap all go through here.
public override ClipShape ClipShape
{
get => base.ClipShape;
set => base.ClipShape = VerticalShapeLocked ? ClipShape.Traditional : value;
}
public override void ToggleClipShape()
{
if (VerticalShapeLocked)
{
ClipShape = ClipShape.Traditional;
return;
}
base.ToggleClipShape();
}
// The geometry is clamped into the band while the vertical placement is
// live — a camera outside the cropped window is a camera the viewers never
// see, and every writer (drag, resize handle, the properties fields) goes
// through these setters.
public override double X
{
get => base.X;
set => base.X = ClampToBand(value, base.Width, vertical: true);
}
public override double Y
{
get => base.Y;
set => base.Y = ClampToBand(value, base.Height, vertical: false);
}
public override double Width
{
get => base.Width;
set => base.Width = VerticalLayoutLive && !VerticalBand.IsEmpty
? Math.Min(value, VerticalBand.Width)
: value;
}
public override double Height
{
get => base.Height;
set => base.Height = VerticalLayoutLive && !VerticalBand.IsEmpty
? Math.Min(value, VerticalBand.Height / 2)
: value;
}
private double ClampToBand(double value, double size, bool vertical)
{
if (!VerticalLayoutLive || VerticalBand.IsEmpty) return value;
var band = VerticalBand;
var min = vertical ? band.Left : band.Top;
var max = vertical ? band.Right - size : band.Bottom - size;
if (max < min) return min;
return Math.Clamp(value, min, max);
}
}