diff --git a/MainWindow.xaml b/MainWindow.xaml
index 6ee6c7e..cae6c48 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -8,7 +8,7 @@
mc:Ignorable="d"
Title="{Binding WindowTitle}" Height="720" Width="1280"
MinWidth="1180" MinHeight="600"
- Icon="/Assets/llama-logo-icon.png"
+ Icon="/ytLive;component/Assets/llama-logo-icon.png"
Background="#1a1a2e"
WindowStartupLocation="CenterScreen"
Closing="MainWindow_Closing"
@@ -323,6 +323,7 @@
@@ -348,25 +349,29 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -687,7 +692,7 @@
-
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 5d09f14..cb8017e 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -153,7 +153,7 @@ public partial class MainWindow : Window
if (selected is { } sel && IsDraggableSource(sel) && HitHandle(e.GetPosition(grid), sel))
{
_isResizing = true;
- _resizeAspect = sel.Width / Math.Max(1, sel.Height);
+ _resizeAspect = sel.ClipShape == ClipShape.Round ? 1 : sel.Width / Math.Max(1, sel.Height);
grid.CaptureMouse();
e.Handled = true;
return;
diff --git a/ai.md b/ai.md
index a63132f..be70fe6 100644
--- a/ai.md
+++ b/ai.md
@@ -110,8 +110,16 @@ C# / WPF (.NET 8) following MVVM:
device never drowns the render thread.
- **Clip/mirror:** per-Source `ClipShape` (Traditional rectangle / Round ellipse) + `IsMirrored`
(`ScaleX = -1`). Rendered in the preview DataTemplate (Image for Traditional, `ImageBrush` inside an
- `Ellipse` for Round); toggled from the source chip; persisted in the layout DB. Round hit-testing is the
- same rectangle as Traditional (selection overlay is rectangular) — acceptable for now.
+ `Ellipse` for Round); toggled from the source chip; persisted in the layout DB.
+ - The Round `Ellipse` is wrapped in a `Viewbox Stretch="Uniform"` holding a `1x1` Grid, so it renders
+ as a true circle (diameter = the shorter source dimension) instead of an oval stretched to the
+ source rect — and the traditional `Image` keeps `UniformToFill` over the full rect.
+ - Resizing locks to a square (`_resizeAspect = 1`) while `ClipShape == Round`.
+ - **Hit-testing:** a `Grid` without `Background` only hit-tests where its children draw, so clicks in
+ the empty corners of a round clip fell through to `Window_PreviewMouseLeftButtonDown` and deselected
+ the source — making the corner handle ungrabbable. The source Grid carries `Background="Transparent"`
+ (whole rect draggable) and the `SelectionOverlay` (dashed border + corner dot) is
+ `IsHitTestVisible="False"` so it never intercepts the click.
- **GPU posture:** webcam frames are CPU (GPU-agnostic; WPF hardware-presents the preview anyway). Hardware
encoders (NVENC/AMF/QSV) matter for the encoder task, not capture. D3DImage GPU compositing is deferred
to the encoder task.
diff --git a/ytLive.Tests/RoundClipInteractionTests.cs b/ytLive.Tests/RoundClipInteractionTests.cs
new file mode 100644
index 0000000..c1379f8
--- /dev/null
+++ b/ytLive.Tests/RoundClipInteractionTests.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using System.Windows.Threading;
+using Xunit;
+using ytLive.Models;
+using ytLive.ViewModels;
+
+namespace ytLive.Tests;
+
+///
+/// Reproduces the real MainWindow preview to check the round-clip corner
+/// handle: whether a click at the corner actually stays inside the preview
+/// (doesn't fall through and deselect the source) and whether the round clip
+/// renders as a circle rather than an oval.
+///
+public sealed class RoundClipInteractionTests
+{
+ [Fact]
+ public void Round_Clip_Corner_Is_Grabbable_And_Shape_Is_Circle()
+ {
+ Exception? failure = null;
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ Run();
+ }
+ catch (Exception ex)
+ {
+ failure = ex;
+ }
+ });
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+
+ if (failure != null)
+ throw new Xunit.Sdk.XunitException("Round-clip interaction failed: " + failure);
+ }
+
+ private void Run()
+ {
+ var app = new App();
+ app.InitializeComponent();
+
+ var window = new MainWindow();
+ var vm = (MainViewModel)window.DataContext;
+ try
+ {
+ window.Show();
+ window.UpdateLayout();
+
+ var scene = vm.ActiveScene!;
+ var source = new Source
+ {
+ Name = "Webcam",
+ Type = SourceType.Webcam,
+ DeviceId = "test-camera",
+ X = 1408,
+ Y = 778,
+ Width = 480,
+ Height = 270,
+ };
+ scene.Sources.Add(source);
+ vm.SelectedSource = source;
+ window.UpdateLayout();
+
+ var previewGrid = (Grid)window.FindName("PreviewGrid")!;
+ var canvasGrid = (Grid)window.FindName("CanvasGrid")!;
+ var toWindow = canvasGrid.TransformToVisual(window);
+
+ var corner = new Point(source.X + source.Width, source.Y + source.Height);
+
+ foreach (var shape in new[] { ClipShape.Traditional, ClipShape.Round })
+ {
+ source.ClipShape = shape;
+ window.UpdateLayout();
+
+ var cornerInWindow = toWindow.Transform(corner);
+
+ // A click on the corner must land inside the preview (descendant
+ // of PreviewGrid); otherwise Window_PreviewMouseLeftButtonDown
+ // deselects the source and the handle can never be grabbed.
+ var hit = VisualTreeHelper.HitTest(window, cornerInWindow);
+ var grabbable = hit != null && IsDescendantOf(hit.VisualHit, previewGrid);
+
+ Assert.True(grabbable,
+ $"{shape}: corner click fell through to '{hit?.VisualHit.GetType().Name ?? "null"}' " +
+ "so the source gets deselected before the resize handler runs");
+ }
+
+ // The round clip must render as a circle (square bounding box), not an oval.
+ source.ClipShape = ClipShape.Round;
+ window.UpdateLayout();
+ var ellipse = FindRoundEllipse(window);
+ Assert.NotNull(ellipse);
+ Assert.Equal(ellipse!.RenderSize.Width, ellipse.RenderSize.Height, 1.0);
+ }
+ finally
+ {
+ window.Close();
+ }
+ }
+
+ private static Ellipse? FindRoundEllipse(Window window)
+ {
+ return Walk(window, element => element is Ellipse e && e.IsVisible) as Ellipse;
+ }
+
+ private static DependencyObject? Walk(DependencyObject parent, Func predicate)
+ {
+ for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
+ {
+ var child = VisualTreeHelper.GetChild(parent, i);
+ if (predicate(child)) return child;
+ var found = Walk(child, predicate);
+ if (found != null) return found;
+ }
+ return null;
+ }
+
+ private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor)
+ {
+ while (child != null && !ReferenceEquals(child, ancestor))
+ child = VisualTreeHelper.GetParent(child);
+ return child != null;
+ }
+}