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:
+8
-132
@@ -84,122 +84,10 @@ public partial class MainViewModel : ViewModelBase
|
||||
public bool IsZoomActive => _zoomState.IsActive;
|
||||
public bool ShowZoomOverlay => _zoomState.IsActive;
|
||||
|
||||
public void ActivateZoom(double clickX, double clickY)
|
||||
{
|
||||
if (!(IsLive || IsRecording)) return;
|
||||
var maxW = MasterFrameWidth * 0.7;
|
||||
var maxH = MasterFrameHeight * 0.7;
|
||||
double w, h;
|
||||
switch (_zoomShape)
|
||||
{
|
||||
case ZoomShape.Circle:
|
||||
var size = Math.Min(maxW, maxH);
|
||||
w = size; h = size;
|
||||
break;
|
||||
case ZoomShape.VerticalRectangle:
|
||||
h = maxH;
|
||||
w = h * 9.0 / 16.0;
|
||||
break;
|
||||
default:
|
||||
w = maxW;
|
||||
h = w * 9.0 / 16.0;
|
||||
break;
|
||||
}
|
||||
double x, y;
|
||||
if (_zoomShape == ZoomShape.Circle)
|
||||
{
|
||||
x = clickX - w / 2;
|
||||
y = clickY - h / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = clickX;
|
||||
y = clickY;
|
||||
}
|
||||
x = Math.Clamp(x, 0, MasterFrameWidth - w);
|
||||
y = Math.Clamp(y, 0, MasterFrameHeight - h);
|
||||
_zoomState.X = x;
|
||||
_zoomState.Y = y;
|
||||
_zoomState.Width = w;
|
||||
_zoomState.Height = h;
|
||||
_zoomState.IsActive = true;
|
||||
_zoomState.IsMoving = false;
|
||||
RefreshZoomUi();
|
||||
}
|
||||
// Zoom activation/move/dismiss and the zoom frame builder live in
|
||||
// MainViewModel.Zoom.cs — the zoom box is a real composited layer (see TASK 51).
|
||||
|
||||
public void ToggleZoom()
|
||||
{
|
||||
if (_zoomState.IsActive)
|
||||
{
|
||||
_zoomState.IsActive = false;
|
||||
_zoomState.IsMoving = false;
|
||||
RefreshZoomUi();
|
||||
}
|
||||
}
|
||||
|
||||
public void DismissZoom()
|
||||
{
|
||||
_zoomState.IsActive = false;
|
||||
_zoomState.IsMoving = false;
|
||||
RefreshZoomUi();
|
||||
}
|
||||
|
||||
public void StartZoomMove(double x, double y)
|
||||
{
|
||||
if (!_zoomState.IsActive) return;
|
||||
_zoomState.IsMoving = true;
|
||||
_zoomState.GrabOffsetX = x - _zoomState.X;
|
||||
_zoomState.GrabOffsetY = y - _zoomState.Y;
|
||||
OnPropertyChanged(nameof(ZoomState));
|
||||
}
|
||||
|
||||
public void UpdateZoomMove(double x, double y)
|
||||
{
|
||||
if (!_zoomState.IsActive || !_zoomState.IsMoving) return;
|
||||
var nx = x - _zoomState.GrabOffsetX;
|
||||
var ny = y - _zoomState.GrabOffsetY;
|
||||
_zoomState.X = Math.Clamp(nx, 0, MasterFrameWidth - _zoomState.Width);
|
||||
_zoomState.Y = Math.Clamp(ny, 0, MasterFrameHeight - _zoomState.Height);
|
||||
OnPropertyChanged(nameof(ZoomState));
|
||||
RebuildDimRectsForZoom();
|
||||
}
|
||||
|
||||
public void EndZoomMove()
|
||||
{
|
||||
if (_zoomState.IsMoving)
|
||||
{
|
||||
_zoomState.IsMoving = false;
|
||||
OnPropertyChanged(nameof(ZoomState));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshZoomUi()
|
||||
{
|
||||
OnPropertyChanged(nameof(ZoomState));
|
||||
OnPropertyChanged(nameof(IsZoomActive));
|
||||
OnPropertyChanged(nameof(ShowZoomOverlay));
|
||||
RebuildDimRectsForZoom();
|
||||
}
|
||||
|
||||
private void RebuildDimRectsForZoom()
|
||||
{
|
||||
if (_zoomState.IsActive)
|
||||
{
|
||||
var zx = _zoomState.X;
|
||||
var zy = _zoomState.Y;
|
||||
var zw = _zoomState.Width;
|
||||
var zh = _zoomState.Height;
|
||||
DimRects.Clear();
|
||||
if (zx > 0) DimRects.Add(new Rect(0, 0, zx, MasterFrameHeight));
|
||||
var rx = zx + zw;
|
||||
if (rx < MasterFrameWidth) DimRects.Add(new Rect(rx, 0, MasterFrameWidth - rx, MasterFrameHeight));
|
||||
if (zy > 0) DimRects.Add(new Rect(0, 0, MasterFrameWidth, zy));
|
||||
var ry = zy + zh;
|
||||
if (ry < MasterFrameHeight) DimRects.Add(new Rect(0, ry, MasterFrameWidth, MasterFrameHeight - ry));
|
||||
return;
|
||||
}
|
||||
RebuildQualityDimRects();
|
||||
} private void RebuildQualityDimRects()
|
||||
private void RebuildQualityDimRects()
|
||||
{
|
||||
DimRects.Clear();
|
||||
if (OutputRectWidth <= 0 || OutputRectHeight <= 0) return;
|
||||
@@ -513,6 +401,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
AddScene(name);
|
||||
}
|
||||
HealBackgrounds();
|
||||
HealZoomLayers();
|
||||
RefreshHotkeyLabels();
|
||||
|
||||
foreach (var scene in Scenes)
|
||||
@@ -688,31 +577,18 @@ public partial class MainViewModel : ViewModelBase
|
||||
Source { Type: SourceType.WebSource } web => _webView2Manager?.GetLatestFrame(web.Id),
|
||||
Source { IsMediaSource: true, MediaPath: not null } media => _mediaManager.GetLatestFrame(media.MediaPath),
|
||||
Source { AssetId: not null } image => StaticPixelCache.Get(image.AssetId),
|
||||
ZoomSceneElement zoom when zoom.IsZoomActive => BuildZoomFrame(zoom),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// The tier's crop rect over the 1920x1080 master, integer-aligned (the VM's
|
||||
// OutputRect* are doubles — the vertical 607.5 half-pixel crop rounds to 608).
|
||||
private CompositorOptions BuildCompositorOptions()
|
||||
// internal = test seam (the zoom layer must NOT change the output crop — the
|
||||
// box is a composited layer, never the crop source).
|
||||
internal CompositorOptions BuildCompositorOptions()
|
||||
{
|
||||
var quality = SelectedQuality;
|
||||
if (_zoomState.IsActive)
|
||||
{
|
||||
var zx = _zoomState.X;
|
||||
var zy = _zoomState.Y;
|
||||
var zw = _zoomState.Width;
|
||||
var zh = _zoomState.Height;
|
||||
return new CompositorOptions
|
||||
{
|
||||
SourceRectX = (int)Math.Clamp(Math.Round(zx), 0, 1919),
|
||||
SourceRectY = (int)Math.Clamp(Math.Round(zy), 0, 1079),
|
||||
SourceRectWidth = (int)Math.Max(1, Math.Round(zw)),
|
||||
SourceRectHeight = (int)Math.Max(1, Math.Round(zh)),
|
||||
OutputWidth = quality.Width,
|
||||
OutputHeight = quality.Height,
|
||||
};
|
||||
}
|
||||
return new CompositorOptions
|
||||
{
|
||||
SourceRectX = (int)Math.Round(OutputRectX),
|
||||
|
||||
Reference in New Issue
Block a user