2f135687cd
Same root cause as IsChatBox — SourceType binding fails on WebcamSceneConfig, WPF defaults Visibility to Visible without calling the converter. Added virtual bool IsCountdown on SceneElement, override in Source. Swapped binding to BoolToVis. Removed dead SourceTypeToVisibility converters.
255 lines
7.6 KiB
C#
255 lines
7.6 KiB
C#
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
|
|
namespace ytLive.Models;
|
|
|
|
public enum BorderAnimation
|
|
{
|
|
None,
|
|
Pulse,
|
|
Chase,
|
|
Rainbow,
|
|
Shimmer,
|
|
MarchingAnts,
|
|
Glow,
|
|
Electricity,
|
|
Sparkles
|
|
}
|
|
|
|
/// <summary>
|
|
/// A thing rendered in a scene: a multi-instance Source (image/background/text)
|
|
/// or a singleton resource's per-scene config (webcam). Carries the shared
|
|
/// layout + clip/mirror + border surface the preview pipeline renders from.
|
|
/// </summary>
|
|
public abstract class SceneElement : INotifyPropertyChanged
|
|
{
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
protected void Raise([CallerMemberName] string? name = null)
|
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
|
|
protected bool Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
|
|
{
|
|
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
|
field = value;
|
|
Raise(name);
|
|
return true;
|
|
}
|
|
|
|
public string Id { get; init; } = Guid.NewGuid().ToString();
|
|
|
|
private string _name = string.Empty;
|
|
public string Name { get => _name; set => Set(ref _name, value); }
|
|
|
|
/// <summary>True only for singleton resources (webcam). Gates webcam-only UI.</summary>
|
|
public virtual bool IsWebcam => false;
|
|
|
|
/// <summary>True only for WebSource-type elements. Gates URI input visibility.</summary>
|
|
public virtual bool IsWebSource => false;
|
|
|
|
/// <summary>True for elements that can be dragged/resized in the preview (image, webcam).</summary>
|
|
public virtual bool IsDraggable => false;
|
|
|
|
/// <summary>True only for image sources — the raw preview renderer's discriminator.</summary>
|
|
public virtual bool IsImageSource => false;
|
|
|
|
/// <summary>True only for ChatBox-type sources. Gates chat overlay properties panel.</summary>
|
|
public virtual bool IsChatBox => false;
|
|
|
|
/// <summary>True only for Countdown-type sources. Gates timer properties panel.</summary>
|
|
public virtual bool IsCountdown => false;
|
|
|
|
private bool _isVisible = true;
|
|
public bool IsVisible { get => _isVisible; set => Set(ref _isVisible, value); }
|
|
|
|
private bool _isEditing;
|
|
public bool IsEditing { get => _isEditing; set => Set(ref _isEditing, value); }
|
|
|
|
private ClipShape _clipShape = ClipShape.Traditional;
|
|
public ClipShape ClipShape
|
|
{
|
|
get => _clipShape;
|
|
set
|
|
{
|
|
if (Set(ref _clipShape, value))
|
|
Raise(nameof(ShapeButtonText));
|
|
}
|
|
}
|
|
|
|
// The rectangular dimensions before a Round toggle, so switching back restores
|
|
// the aspect instead of staying locked to the square Round used. Persisted so
|
|
// the restore survives a reload of a Round element.
|
|
private double? _rectWidth;
|
|
private double? _rectHeight;
|
|
|
|
public double? RectWidth { get => _rectWidth; set => Set(ref _rectWidth, value); }
|
|
public double? RectHeight { get => _rectHeight; set => Set(ref _rectHeight, value); }
|
|
|
|
public void ToggleClipShape()
|
|
{
|
|
if (ClipShape == ClipShape.Traditional)
|
|
{
|
|
RectWidth = Width;
|
|
RectHeight = Height;
|
|
ClipShape = ClipShape.Round;
|
|
}
|
|
else
|
|
{
|
|
ClipShape = ClipShape.Traditional;
|
|
if (RectWidth is { } rw && RectHeight is { } rh)
|
|
{
|
|
Width = rw;
|
|
Height = rh;
|
|
RectWidth = null;
|
|
RectHeight = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool _isMirrored;
|
|
public bool IsMirrored
|
|
{
|
|
get => _isMirrored;
|
|
set
|
|
{
|
|
if (Set(ref _isMirrored, value))
|
|
{
|
|
Raise(nameof(MirrorScale));
|
|
Raise(nameof(MirrorButtonText));
|
|
}
|
|
}
|
|
}
|
|
|
|
public double MirrorScale => IsMirrored ? -1 : 1;
|
|
public string MirrorButtonText => IsMirrored ? "Unmirror" : "Mirror";
|
|
public string ShapeButtonText => ClipShape == ClipShape.Round ? "Rect" : "Round";
|
|
|
|
// Live camera frames: the shared WriteableBitmap owned by CameraManager.
|
|
private WriteableBitmap? _videoImageSource;
|
|
public WriteableBitmap? VideoImageSource
|
|
{
|
|
get => _videoImageSource;
|
|
set
|
|
{
|
|
if (Set(ref _videoImageSource, value))
|
|
Raise(nameof(DisplaySource));
|
|
}
|
|
}
|
|
|
|
/// <summary>What the preview shows: live frames (webcam) or a static image (sources).</summary>
|
|
public abstract ImageSource? DisplaySource { get; }
|
|
|
|
// Position/transform (per-scene usage)
|
|
private double _x;
|
|
public double X { get => _x; set => Set(ref _x, value); }
|
|
|
|
private double _y;
|
|
public double Y { get => _y; set => Set(ref _y, value); }
|
|
|
|
private double _width;
|
|
public double Width
|
|
{
|
|
get => _width;
|
|
set
|
|
{
|
|
if (Set(ref _width, value))
|
|
Raise(nameof(RoundBorderSize));
|
|
}
|
|
}
|
|
|
|
private double _height;
|
|
public double Height
|
|
{
|
|
get => _height;
|
|
set
|
|
{
|
|
if (Set(ref _height, value))
|
|
Raise(nameof(RoundBorderSize));
|
|
}
|
|
}
|
|
|
|
private double _opacity = 1.0;
|
|
public double Opacity { get => _opacity; set => Set(ref _opacity, value); }
|
|
|
|
/// <summary>Round clip/border diameter = the shorter dimension (true circle).</summary>
|
|
public double RoundBorderSize => Math.Min(Width, Height);
|
|
|
|
// Border (OBS-style): #RRGGBB color, alpha opacity, pixel width. Off by default.
|
|
private string _borderColor = string.Empty;
|
|
public string BorderColor
|
|
{
|
|
get => _borderColor;
|
|
set
|
|
{
|
|
if (Set(ref _borderColor, value))
|
|
{
|
|
Raise(nameof(HasBorder));
|
|
Raise(nameof(BorderBrush));
|
|
}
|
|
}
|
|
}
|
|
|
|
private double _borderOpacity = 1.0;
|
|
public double BorderOpacity
|
|
{
|
|
get => _borderOpacity;
|
|
set
|
|
{
|
|
if (Set(ref _borderOpacity, value))
|
|
{
|
|
Raise(nameof(HasBorder));
|
|
Raise(nameof(BorderBrush));
|
|
}
|
|
}
|
|
}
|
|
|
|
private int _borderWidth;
|
|
public int BorderWidth
|
|
{
|
|
get => _borderWidth;
|
|
set
|
|
{
|
|
if (Set(ref _borderWidth, value))
|
|
{
|
|
Raise(nameof(HasBorder));
|
|
Raise(nameof(BorderBrush));
|
|
}
|
|
}
|
|
}
|
|
|
|
private BorderAnimation _borderAnimation = BorderAnimation.None;
|
|
public BorderAnimation BorderAnimation { get => _borderAnimation; set => Set(ref _borderAnimation, value); }
|
|
|
|
public bool HasBorder
|
|
{
|
|
get
|
|
{
|
|
if (BorderWidth <= 0) return false;
|
|
return TryGetBorderColor(out _, out _, out _);
|
|
}
|
|
}
|
|
|
|
public Brush? BorderBrush
|
|
{
|
|
get
|
|
{
|
|
if (!HasBorder || !TryGetBorderColor(out var r, out var g, out var b)) return null;
|
|
var alpha = (byte)Math.Round(Math.Clamp(BorderOpacity, 0, 1) * 255);
|
|
return new SolidColorBrush(Color.FromArgb(alpha, r, g, b));
|
|
}
|
|
}
|
|
|
|
public bool TryGetBorderColor(out byte r, out byte g, out byte b)
|
|
{
|
|
r = g = b = 0;
|
|
var hex = BorderColor.Trim().TrimStart('#');
|
|
if (hex.Length != 6) return false;
|
|
return byte.TryParse(hex.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out r)
|
|
&& byte.TryParse(hex.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out g)
|
|
&& byte.TryParse(hex.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b);
|
|
}
|
|
}
|