Zoom: dims never shade the box — move the dim overlay above CanvasGrid (Unit: zoom shading fix)

Creator's final zoom correction: as the box is dragged, its capture region slides
under the dim rectangles and the box picks up the 55% black "div shading" inside
itself, because the VisualBrush samples the whole CanvasGrid and DimRects lived
inside CanvasGrid. The captured area must stay clear regardless of box position.

- PreviewPane.xaml: the DimRects ItemsControl is now a SIBLING of CanvasGrid
  (above the composed content, below the ZoomBox), never a descendant — the zoom
  source is shading-free by construction.
- ZoomBoxInteractionTests: structural invariant — DimRectsLayer is a direct
  child of PreviewCanvas (not inside CanvasGrid) and renders below the ZoomBox.
- Side effect (correctness, not a bug): quality dims now also sedate the global
  overlays outside the crop, matching "chrome outside the crop is dimmed".
This commit is contained in:
2026-10-08 08:40:23 -07:00
parent 6bff21b234
commit 948dd08214
4 changed files with 70 additions and 55 deletions
+27 -20
View File
@@ -316,26 +316,6 @@
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,-13,-13"/>
</Grid>
<ItemsControl ItemsSource="{Binding DimRects}" IsHitTestVisible="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Width="{Binding Width}" Height="{Binding Height}"
Fill="#000000" Opacity="0.55"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Rectangle IsHitTestVisible="False" Stroke="#e94560" StrokeThickness="3"
Canvas.Left="{Binding OutputRectX}" Canvas.Top="{Binding OutputRectY}"
Width="{Binding OutputRectWidth}" Height="{Binding OutputRectHeight}"
@@ -410,6 +390,33 @@
</Canvas>
</Grid>
<!-- The zoom dims: preview chrome ONLY — a SIBLING of CanvasGrid,
never a descendant — so the zoom box's VisualBrush sample of
CanvasGrid can never pick up the dim shading. The box must show
the captured area clean at all times, even when dragged over the
dims (creator ruling 2026-10-08). Renders above the composed
content (dims the whole scene outside the box incl. the global
overlays) and below the ZoomBox. -->
<ItemsControl x:Name="DimRectsLayer" ItemsSource="{Binding DimRects}" IsHitTestVisible="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Width="{Binding Width}" Height="{Binding Height}"
Fill="#000000" Opacity="0.55"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- The zoom box: layer n+1 — a SIBLING of CanvasGrid (not a
descendant), so its VisualBrush can sample the whole
composed preview (CanvasGrid = backdrop + every element
+25 -35
View File
@@ -1,4 +1,4 @@
# HANDOFF — 2026-10-08 (zoom rework)
# HANDOFF — 2026-10-08 (zoom rework + dims-shading fix)
## Branch
- main (no feature branch)
@@ -9,51 +9,41 @@
- `Controls/LeftPanel.xaml` (pinned-message UI)
- Untracked: `Helpers/RelayCommandGeneric.cs`
## What just happened (2026-10-08, second zoom pass — creator correction)
The zoom box shipped in `4b3d82f` was a **loupe** (box follows its own content, built from the
backdrop only, slotted at z-index 1). The creator corrected it; the model is now:
## What just happened (2026-10-08, third zoom pass — dims shading)
The zoom box shipped as a **loupe** (`4b3d82f`), then was corrected to an **anchored-capture
magnifier over the full composite drawn last** (commit `6bff21b`). The final correction: as the box
is dragged, the dim rectangles (55% black "div shading" around the box) were sliding under the
capture region — shown inside the box because its `VisualBrush` samples all of `CanvasGrid`, and the
`DimRects` ItemsControl previously lived INSIDE `CanvasGrid`.
- **Anchored capture, not a loupe.** Right-clicking the live preview fixes a **capture window** —
the box/2 region centred on the click (`CaptureX/Y`, clamped to the master) — and parks a movable
**2x magnifier** box. The box is a **live, always-on 2x view of that captured region**; dragging the
box relocates it, **`UpdateZoomMove` never touches the capture** (creator ruling).
- **Content = every layer.** The zoom pass runs on the **already-composited frame** (backdrop + all
elements), so the magnified webcam/square/layers all show in the box. `SceneCompositor` skips the
active `ZoomSceneElement` in every layer loop and `BlitZoom`s it **LAST (layer n+1, on top of
everything)** via the pure static `ZoomMagnifier` (2x bilinear, 8.8 fixed-point, temp snapshot via
`ArrayPool` so a box overlapping its own source never recurses, optional round clip).
- **Rectangle = 4:3** ("more of a 640x480 aspect ratio"); Circle = square, Phone = 9:16 (kept).
Changes in this pass: new `Services/Compositor/ZoomMagnifier.cs`; `ZoomSceneElement`/`ZoomState`
gained `CaptureX`/`CaptureY`; `MainViewModel.Zoom.cs` rewritten (`BuildZoomFrame`/`ZoomFrameBuilder`
and the zoom-as-crop resolver case removed — no per-frame builder/cache anymore);
`PreviewPane.xaml`: `ZoomBox` hoisted OUT of `OverlayCanvas` to be a **sibling of `CanvasGrid`**
(renders above everything in the preview); `PreviewPane.xaml.cs` `UpdateZoomBox` samples `CanvasGrid`
(the whole composed preview) at the fixed capture, `Stretch=Fill`. `ZoomFrameBuilderTests.cs` deleted
(superseded); new `ZoomMagnifierTests.cs` (the "box over blue shows green capture, at 2x, all layers"
proof + round-clip); `ZoomBoxInteractionTests.cs` rewritten (per-shape 4:3/9:16/square + capture
anchor centred on the click + move-moves-only-the-box assertion + corner-clamp).
Fix: the `DimRects` overlay is now a **sibling of `CanvasGrid`** (renders above the composed
content, below the `ZoomBox`), never a descendant — so the zoom sample can never pick up the shading
and the captured area stays clear no matter where the box is dragged. Behavior consequence to note:
in non-zoom ("quality dims") mode the dims now also shade the global overlays (brand flash, alert
ticker, social bar) that sit above them in the old tree — consistent with "chrome outside the crop
is dimmed". Structural invariant added to `ZoomBoxInteractionTests`: `DimRectsLayer` is a direct
child of `PreviewCanvas`, not inside `CanvasGrid`, and renders below the `ZoomBox`.
## Zoom feature state
- Models: `ZoomState.cs`, `ZoomShape.cs` (Circle/VerticalRectangle/HorizontalRectangle), `ZoomSceneElement.cs`.
- ViewModel (`MainViewModel.Zoom.cs`): `ActivateZoom`/`ToggleZoom`/`DismissZoom`/`StartZoomMove`/`UpdateZoomMove`/`EndZoomMove` + `RebuildDimRectsForZoom`; transient element healed/mirrored by `HealZoomLayers`/`EnsureZoomLayer`.
- Output: box composited last via `BlitZoom`/`ZoomMagnifier`; output crop = tier (box never the crop).
- Preview: `ZoomBox` = sibling above `CanvasGrid`, samples the full composite at the fixed capture, 2x; circle→ellipse clip, rect/square outline per shape; `LeftPanel` hides the placeholder row via `IsZoomLayer`.
- Preview: `ZoomBox` + `DimRectsLayer` are both SIBLINGS of `CanvasGrid` (box above dims above content); box samples the full composite at the fixed capture, 2x; circle→ellipse clip, rect/square outline per shape; `LeftPanel` hides the placeholder row via `IsZoomLayer`.
- Rectangle shape = 4:3; Circle square; Phone 9:16. Capture anchored on click (`CaptureX/Y`); dragging the box never changes it.
## Tests
- Full suite: **404 passed / 2 failed** (both = known pre-existing environment flakes, unrelated to
zoom): `AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` (audio timing)
and `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` (needs
interactive desktop). Zoom tests 3/3 green, 0 build warnings.
- Full suite: **406 passed / 0 failed** on a clean full run (the pre-existing audio-loopback timing
flake and `RealMouseDrag`'s interactive-desktop requirement recur occasionally — not zoom
regressions). Zoom tests 3/3 green, 0 build warnings.
## Known issues / follow-ups
- **Background "Use default background" toggle still not working** (carried from 2026-10-07):
binding exists (`UseDefaultBackground`), VM handler invalidates bake; investigate when time permits.
- Preview shows editing chrome (dim rects, selection) inside the zoom box — accepted preview
artefact; the output only shows composed content.
- The 3px selection / crop-outline strokes could still show in the zoom box IF the capture overlaps
them in edit mode — not shading, thin outlines only; the dims (the user's complaint) are gone.
## Next step
- User runs the app live to eyeball the corrected zoom: right-click preview → anchored capture stays
fixed while the box is dragged; the box shows ALL layers at 2x on top of everything; rectangle box
is 4:3. Then TASK 51 (webcam background Remove + Blur) per `TASKS/task-51-webcam-bg-effects.md`,
and the background-toggle investigation if still open.
- User runs the app live to eyeball: drag the zoom box anywhere and the captured content stays clean
(no dim shading in the box; the scene outside the box stays dimmed). Then TASK 51 (webcam
background Remove + Blur) per `TASKS/task-51-webcam-bg-effects.md`, and the background-toggle
investigation if still open.
+5
View File
@@ -737,6 +737,11 @@ parallel software compositor** over the `VideoFrame` (BGRA8) seam, and the XAML
renderers must agree: geometry, `UniformToFill` cover-crop, round clip, mirror, border, z-order. Preview
stays XAML (editing view); the compositor is the output view.
- **The `DimRects` shading is itself preview chrome, so it renders as a SIBLING of `CanvasGrid`
(above it, below the `ZoomBox`), never a descendant** (2026-10-08, creator ruling): the zoom box's
`VisualBrush` samples all of `CanvasGrid`, and if the dims were inside it the box would show the
dim shading — especially after the box is dragged away and the fixed capture slides under the dims.
- **Render the active tier's output rect directly** (`CompositorOptions {SourceRectX/Y/W/H,
OutputWidth, OutputHeight}`, fed from `MainViewModel.OutputRect*`): 16:9 = full 1920×1080 1:1; the
vertical 9:16 tier = composite the centered 607×1080 crop then bilinear-upscale to 1080×1920.
+13
View File
@@ -77,6 +77,19 @@ public sealed class ZoomBoxInteractionTests
Assert.Equal(Visibility.Visible, circleOutline.Visibility);
Assert.Equal(Visibility.Collapsed, rectOutline.Visibility);
// The dims are preview chrome ABOVE the composed content (a sibling of
// CanvasGrid, below the ZoomBox) — never inside CanvasGrid — so the zoom
// box's sample of CanvasGrid can never pick up the dim shading, even when
// the zoom box is dragged so the capture slides under the dims.
var previewCanvas = (Canvas)pane.FindName("PreviewCanvas")!;
var canvasGrid = (Grid)pane.FindName("CanvasGrid")!;
var dimsLayer = (ItemsControl)pane.FindName("DimRectsLayer")!;
Assert.Equal(previewCanvas, VisualTreeHelper.GetParent(dimsLayer));
for (var parent = VisualTreeHelper.GetParent(dimsLayer); parent != null; parent = VisualTreeHelper.GetParent(parent))
Assert.False(ReferenceEquals(parent, canvasGrid), "the dims layer must not be inside CanvasGrid");
Assert.True(previewCanvas.Children.IndexOf(dimsLayer) < previewCanvas.Children.IndexOf(zoomBox),
"the dims must render below the zoom box, not above it");
// Phone (vertical rectangle): tall 9:16 box; capture centred on the click.
vm.ZoomShape = ZoomShape.VerticalRectangle;
vm.ActivateZoom(960, 540);