Zoom: compose the box as a real 2x layer above the background (Unit: zoom layer + magnification proof)

Right-click now creates a transient ZoomSceneElement at z-index 1 whose pixels are a
genuine 2x magnified sample of the backdrop (live capture under art), composited into
stream/recording like any layer while the output crop stays the tier. All shapes are
centered on the click and clamped to frame; healed on load/scene-create, never persisted,
hidden from the layers list + element template (IsZoomLayer). BuildCompositorOptions is
now internal = always tier crop (test seam). ZoomFrameBuilder is a pure static sampler
(box/2 window centered on the box center, single-slot cache) proven by a banded-backdrop
test; LayerReorderPersistenceTests drag resolves rows by item since the collapsed zoom
row shifts container indices. Also books TASK 51 (webcam background Remove + Blur) and
syncs memory docs (HANDOFF, MyMistakes, indexes). Full suite: 405 passed, 0 warnings.
This commit is contained in:
2026-10-07 23:39:54 -07:00
parent 966e718f1e
commit 4b3d82ff0f
17 changed files with 734 additions and 210 deletions
+5
View File
@@ -65,6 +65,11 @@
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Opacity" Value="0.45"/>
</DataTrigger>
<!-- The transient zoom layer has no editing UI — never
show it as a layer row (TASK 51). -->
<DataTrigger Binding="{Binding IsZoomLayer}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Helpers:FocusPreservingListBox.ItemContainerStyle>
+27 -17
View File
@@ -63,14 +63,28 @@
</ContextMenu>
</Grid.ContextMenu>
<!-- Live desktop/game background (bottom layer; the static
image renders above it). -->
<Image Source="{Binding BackgroundImage}" Stretch="UniformToFill"
Visibility="{Binding BackgroundVisible, Converter={StaticResource BoolToVis}}"
IsHitTestVisible="False"/>
<Image Source="{Binding ActiveBackgroundImage}" Stretch="UniformToFill"
IsHitTestVisible="False"/>
<Rectangle Stroke="#22c55e" StrokeThickness="6" IsHitTestVisible="False"/>
image renders above it). The zoom magnifier samples
this layer — see UpdateZoomBox (TASK 51). -->
<Grid x:Name="BackdropLayer" IsHitTestVisible="False">
<Image Source="{Binding BackgroundImage}" Stretch="UniformToFill"
Visibility="{Binding BackgroundVisible, Converter={StaticResource BoolToVis}}"/>
<Image Source="{Binding ActiveBackgroundImage}" Stretch="UniformToFill"/>
<Rectangle Stroke="#22c55e" StrokeThickness="6"/>
</Grid>
<Canvas x:Name="OverlayCanvas" Width="1920" Height="1080">
<!-- The zoom box renders below every element layer so the
preview shows exactly what the output composites (the
zoom is a real layer at z-index 1, not an overlay). -->
<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" Stretch="Fill"/>
</Grid.Background>
</Grid>
<ItemsControl ItemsSource="{Binding StagedScene.Elements}"
Width="1920" Height="1080">
<ItemsControl.ItemsPanel>
@@ -103,6 +117,12 @@
<DataTrigger Binding="{Binding IsBackground}" Value="True">
<Setter Property="IsHitTestVisible" Value="False"/>
</DataTrigger>
<!-- The zoom layer never renders here — the ZoomBox magnifier (and the
output resolver) own its pixels (TASK 51). -->
<DataTrigger Binding="{Binding IsZoomLayer}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="IsHitTestVisible" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
@@ -402,16 +422,6 @@
</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"
+10 -3
View File
@@ -553,8 +553,15 @@ public partial class PreviewPane : UserControl
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);
// The preview mirrors the output zoom layer: sample the backdrop layer
// (the live capture + art, exactly what the compositor magnifies) at the
// box/2 interior centered on the box center → 2x on the screen.
ZoomVisualBrush.Visual = BackdropLayer;
ZoomVisualBrush.Viewbox = new Rect(
vm.ZoomState.X + vm.ZoomState.Width / 4,
vm.ZoomState.Y + vm.ZoomState.Height / 4,
vm.ZoomState.Width / 2,
vm.ZoomState.Height / 2);
ZoomVisualBrush.ViewboxUnits = BrushMappingMode.Absolute;
if (vm.ZoomShape == Models.ZoomShape.Circle)
{
@@ -569,7 +576,7 @@ public partial class PreviewPane : UserControl
ZoomBox.Clip = null;
}
}
else if (!vm.IsZoomActive && ZoomVisualBrush != null && ReferenceEquals(ZoomVisualBrush.Visual, CanvasGrid))
else if (!vm.IsZoomActive && ZoomVisualBrush != null && ReferenceEquals(ZoomVisualBrush.Visual, BackdropLayer))
{
ZoomVisualBrush.Visual = null;
}