Zoom: fix preview zoom box + shape-aspect anchoring (Unit: zoom box visuals)
Right-click in the live preview now procs a box that is actually visible and populated with the zoomed region, and each Zoom View Shape sculpts the box per spec. OBS-style derivative: magnifier box shows the composited region and dims the surroundings (same pattern as OBS "zoom" / preview region highlights). Root causes of the invisible box (two WPF traps, recorded in MyMistakes.md): - Empty <VisualBrush Stretch="None"/> used as an OpacityMask paints nothing, so the mask alpha is 0 and the WHOLE box (border included) went transparent. - ZoomBox lived inside CanvasGrid while its VisualBrush sampled CanvasGrid — a circular reference WPF renders as nothing. Hoisted ZoomBox to a sibling of CanvasGrid inside a new PreviewCanvas wrapper (same 1920x1080 space). Shape semantics (MainViewModel.ActivateZoom): - Circle: square, click point = center. - VerticalRectangle (phone): tall 9:16, click point = top-left corner. - HorizontalRectangle (tablet): wide 16:9, click point = top-left corner. - Boxes stay fully inside 1920x1080 via Math.Clamp. UI: ZoomRectOutline/ZoomCircleOutline shapes replace the square Border; circle clips the box to an EllipseGeometry; brush Visual released when inactive. Zoom View Shape dropdown now uses the standard YtComboBox style. Test (Good Dog rule, one integration test): ZoomBoxInteractionTests verifies aspect/anchoring/clamp per shape and the matching outline+clip in the real MainWindow. Full suite 404 passed, 0 warnings. Memory: HANDOFF.md zoom state updated; WPF VisualBrush traps appended to MyMistakes.md.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<UserControl x:Class="ytLive.Controls.OverlayHost"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:Helpers="clr-namespace:ytLive.Helpers">
|
||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"
|
||||
xmlns:Models="clr-namespace:ytLive.Models">
|
||||
<Grid Grid.RowSpan="3"
|
||||
Visibility="{Binding IsAnyOverlayOpen, Converter={StaticResource BoolToVis}}">
|
||||
<Button Background="#99000000" BorderThickness="0" Cursor="Hand"
|
||||
@@ -42,12 +43,18 @@
|
||||
<TextBox Style="{StaticResource YtTextBox}"
|
||||
Text="{Binding RecordFolderDisplay, Mode=OneWay}"
|
||||
IsReadOnly="True" Margin="0,0,0,12" Foreground="#a0a0b0"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||
<Button Content="Browse…" Command="{Binding ChooseRecordFolderCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}" Margin="0,0,8,0"/>
|
||||
<Button Content="Use Downloads" Command="{Binding ResetRecordFolderCommand}"
|
||||
Style="{StaticResource YtButtonSecondary}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Zoom View Shape" Style="{StaticResource YtLabel}" Margin="0,0,0,4"/>
|
||||
<ComboBox Style="{StaticResource YtComboBox}" SelectedValue="{Binding ZoomShape}" SelectedValuePath="Tag">
|
||||
<ComboBoxItem Content="Circle" Tag="{x:Static Models:ZoomShape.Circle}"/>
|
||||
<ComboBoxItem Content="Phone (Vertical Rectangle)" Tag="{x:Static Models:ZoomShape.VerticalRectangle}"/>
|
||||
<ComboBoxItem Content="Tablet (Horizontal Rectangle)" Tag="{x:Static Models:ZoomShape.HorizontalRectangle}"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Report Bug -->
|
||||
|
||||
@@ -14,9 +14,18 @@
|
||||
<Grid x:Name="PreviewGrid" Grid.Row="0"
|
||||
PreviewMouseLeftButtonDown="Preview_MouseLeftButtonDown"
|
||||
PreviewMouseMove="Preview_MouseMove"
|
||||
PreviewMouseLeftButtonUp="Preview_MouseLeftButtonUp">
|
||||
PreviewMouseLeftButtonUp="Preview_MouseLeftButtonUp"
|
||||
PreviewMouseRightButtonDown="Preview_MouseRightButtonDown"
|
||||
PreviewMouseRightButtonUp="Preview_MouseRightButtonUp">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Canvas x:Name="PreviewCanvas" Width="1920" Height="1080" ClipToBounds="True">
|
||||
<Grid x:Name="CanvasGrid" Width="1920" Height="1080" ClipToBounds="True" Background="Transparent">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform x:Name="ZoomScale"/>
|
||||
<TranslateTransform x:Name="ZoomTranslate"/>
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<Grid.ContextMenu>
|
||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||
<MenuItem Header="Show Desktop" IsCheckable="True"
|
||||
@@ -388,11 +397,22 @@
|
||||
Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Canvas>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
<Grid x:Name="ZoomBox" IsHitTestVisible="False"
|
||||
Canvas.Left="{Binding ZoomState.X}" Canvas.Top="{Binding ZoomState.Y}"
|
||||
Width="{Binding ZoomState.Width}" Height="{Binding ZoomState.Height}"
|
||||
Visibility="{Binding IsZoomActive, Converter={StaticResource BoolToVis}}">
|
||||
<Rectangle x:Name="ZoomRectOutline" Stroke="#d8a72b" StrokeThickness="3" Fill="Transparent"/>
|
||||
<Ellipse x:Name="ZoomCircleOutline" Stroke="#d8a72b" StrokeThickness="3" Fill="Transparent" Visibility="Collapsed"/>
|
||||
<Grid.Background>
|
||||
<VisualBrush x:Name="ZoomVisualBrush" ViewboxUnits="Absolute"/>
|
||||
</Grid.Background>
|
||||
</Grid>
|
||||
</Canvas>
|
||||
</Viewbox>
|
||||
<Border HorizontalAlignment="Left" VerticalAlignment="Top" Margin="8,8,0,0"
|
||||
Background="#CCe94560" CornerRadius="4" Padding="8,3" IsHitTestVisible="False"
|
||||
|
||||
@@ -17,6 +17,7 @@ public partial class PreviewPane : UserControl
|
||||
{
|
||||
private bool _isDraggingOverlay;
|
||||
private bool _isResizing;
|
||||
private bool _isZoomDragging;
|
||||
private Point _grabOffset;
|
||||
private double _resizeAspect;
|
||||
|
||||
@@ -29,8 +30,30 @@ public partial class PreviewPane : UserControl
|
||||
public PreviewPane()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += PreviewPane_DataContextChanged;
|
||||
}
|
||||
|
||||
private void PreviewPane_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.OldValue is MainViewModel oldVm)
|
||||
oldVm.PropertyChanged -= ViewModel_PropertyChanged;
|
||||
if (e.NewValue is MainViewModel newVm)
|
||||
{
|
||||
newVm.PropertyChanged += ViewModel_PropertyChanged;
|
||||
UpdateZoomBox();
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.IsZoomActive) || e.PropertyName == nameof(MainViewModel.ZoomState) || e.PropertyName == nameof(MainViewModel.ZoomShape))
|
||||
{
|
||||
UpdateZoomBox();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Static images and the webcam share the move/resize/selection behavior.
|
||||
public static bool IsDraggableElement(SceneElement element)
|
||||
=> element.IsDraggable;
|
||||
@@ -387,12 +410,29 @@ public partial class PreviewPane : UserControl
|
||||
private void Preview_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var vm = DataContext as MainViewModel;
|
||||
if (vm is null || !vm.IsEditMode) return;
|
||||
if (!_isDraggingOverlay && !_isResizing) return;
|
||||
|
||||
if (vm is null) return;
|
||||
var grid = (Grid)sender;
|
||||
var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse;
|
||||
var p = toCanvas.Transform(e.GetPosition(grid));
|
||||
|
||||
if (e.LeftButton == MouseButtonState.Pressed && vm.IsZoomActive)
|
||||
{
|
||||
if (!_isZoomDragging)
|
||||
{
|
||||
vm.StartZoomMove(p.X, p.Y);
|
||||
_isZoomDragging = true;
|
||||
grid.CaptureMouse();
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.UpdateZoomMove(p.X, p.Y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vm.IsEditMode) return;
|
||||
if (!_isDraggingOverlay && !_isResizing) return;
|
||||
|
||||
var selected = vm.SelectedElement;
|
||||
if (selected == null)
|
||||
{
|
||||
@@ -429,14 +469,60 @@ public partial class PreviewPane : UserControl
|
||||
}
|
||||
|
||||
private void Preview_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
=> EndPreviewDrag((Grid)sender);
|
||||
{
|
||||
if (_isZoomDragging && DataContext is MainViewModel vm)
|
||||
{
|
||||
vm.EndZoomMove();
|
||||
_isZoomDragging = false;
|
||||
((Grid)sender).ReleaseMouseCapture();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
EndPreviewDrag((Grid)sender);
|
||||
}
|
||||
|
||||
private void Preview_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var vm = DataContext as MainViewModel;
|
||||
if (vm == null) return;
|
||||
var grid = (Grid)sender;
|
||||
var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse;
|
||||
var p = toCanvas.Transform(e.GetPosition(grid));
|
||||
if ((vm.IsLive || vm.IsRecording) && !vm.IsEditMode)
|
||||
{
|
||||
if (vm.IsZoomActive)
|
||||
{
|
||||
vm.DismissZoom();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
vm.ActivateZoom(p.X, p.Y);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
vm.ToggleZoom();
|
||||
e.Handled = true;
|
||||
} private void Preview_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_isZoomDragging)
|
||||
{
|
||||
var grid = (Grid)sender;
|
||||
grid.ReleaseMouseCapture();
|
||||
_isZoomDragging = false;
|
||||
if (DataContext is MainViewModel vm) vm.EndZoomMove();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void EndPreviewDrag(Grid grid)
|
||||
{
|
||||
if (_isDraggingOverlay || _isResizing)
|
||||
if (_isDraggingOverlay || _isResizing || _isZoomDragging)
|
||||
grid.ReleaseMouseCapture();
|
||||
_isDraggingOverlay = false;
|
||||
_isResizing = false;
|
||||
_isZoomDragging = false;
|
||||
if (DataContext is MainViewModel vm) vm.EndZoomMove();
|
||||
}
|
||||
|
||||
private bool HitHandle(Point mouseScreen, SceneElement element)
|
||||
@@ -461,4 +547,31 @@ public partial class PreviewPane : UserControl
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateZoomBox()
|
||||
{
|
||||
if (DataContext is not MainViewModel vm) return;
|
||||
if (vm.IsZoomActive && ZoomVisualBrush != null)
|
||||
{
|
||||
ZoomVisualBrush.Visual = CanvasGrid;
|
||||
ZoomVisualBrush.Viewbox = new Rect(vm.ZoomState.X, vm.ZoomState.Y, vm.ZoomState.Width, vm.ZoomState.Height);
|
||||
ZoomVisualBrush.ViewboxUnits = BrushMappingMode.Absolute;
|
||||
if (vm.ZoomShape == Models.ZoomShape.Circle)
|
||||
{
|
||||
ZoomCircleOutline.Visibility = Visibility.Visible;
|
||||
ZoomRectOutline.Visibility = Visibility.Collapsed;
|
||||
ZoomBox.Clip = new EllipseGeometry(new Rect(0, 0, vm.ZoomState.Width, vm.ZoomState.Height));
|
||||
}
|
||||
else
|
||||
{
|
||||
ZoomCircleOutline.Visibility = Visibility.Collapsed;
|
||||
ZoomRectOutline.Visibility = Visibility.Visible;
|
||||
ZoomBox.Clip = null;
|
||||
}
|
||||
}
|
||||
else if (!vm.IsZoomActive && ZoomVisualBrush != null && ReferenceEquals(ZoomVisualBrush.Visual, CanvasGrid))
|
||||
{
|
||||
ZoomVisualBrush.Visual = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user