Zoom: anchor the capture, magnify the full composite, draw on top (Unit: zoom rework per creator correction)

Creator corrected the "loupe" zoom on 2026-10-08: the click anchors a fixed
capture region (box/2 centred on the click) and the box is a live 2x view of
that region — dragging the box never changes its content; it magnifies the
fully-composited frame (every layer, incl. webcam) and draws LAST (n+1).
Rectangle shape is 4:3 ("more of a 640x480 aspect").

- MainViewModel.Zoom.cs rewritten: ActivateZoom anchors CaptureX/CaptureY
  (independently clamped); UpdateZoomMove relocates the box only; the per-frame
  BuildZoomFrame/ZoomFrameBuilder path is gone.
- SceneCompositor: skips the active ZoomSceneElement in every layer loop and
  BlitZooms it last via the new pure ZoomMagnifier (2x bilinear, temp snapshot
  via ArrayPool, round clip).
- Preview: ZoomBox hoisted above CanvasGrid as a sibling; its brush samples the
  whole composed preview at the fixed capture (2x).
- Tests: ZoomMagnifierTests (box-over-blue shows green capture at 2x incl. an
  extra red "layer" — the loupe/backdrop-only regressions both fail it) +
  round-clip; ZoomBoxInteractionTests rewritten (anchored capture, move-doesn't-
  change-capture, 4:3/9:16/square). 404 pass, 2 known environment flakes.
This commit is contained in:
2026-10-08 08:26:29 -07:00
parent 4b3d82ff0f
commit 6bff21b234
15 changed files with 467 additions and 330 deletions
+9 -7
View File
@@ -553,13 +553,15 @@ public partial class PreviewPane : UserControl
if (DataContext is not MainViewModel vm) return;
if (vm.IsZoomActive && ZoomVisualBrush != null)
{
// 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;
// The preview mirrors the output zoom layer: sample the WHOLE composed
// preview (CanvasGrid = backdrop + every element layer — the same all-layer
// source the output compositor magnifies) at the FIXED capture window (the
// box/2 region centred on the click) → 2x via Stretch=Fill. Moving the box
// only moves the ZoomBox element; the Viewbox stays glued to the capture.
ZoomVisualBrush.Visual = CanvasGrid;
ZoomVisualBrush.Viewbox = new Rect(
vm.ZoomState.X + vm.ZoomState.Width / 4,
vm.ZoomState.Y + vm.ZoomState.Height / 4,
vm.ZoomState.CaptureX,
vm.ZoomState.CaptureY,
vm.ZoomState.Width / 2,
vm.ZoomState.Height / 2);
ZoomVisualBrush.ViewboxUnits = BrushMappingMode.Absolute;
@@ -576,7 +578,7 @@ public partial class PreviewPane : UserControl
ZoomBox.Clip = null;
}
}
else if (!vm.IsZoomActive && ZoomVisualBrush != null && ReferenceEquals(ZoomVisualBrush.Visual, BackdropLayer))
else if (!vm.IsZoomActive && ZoomVisualBrush != null && ReferenceEquals(ZoomVisualBrush.Visual, CanvasGrid))
{
ZoomVisualBrush.Visual = null;
}