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:
@@ -288,6 +288,7 @@ public partial class MainViewModel
|
||||
HasBackground = SceneCatalog.HasBackground(name),
|
||||
};
|
||||
EnsureBackground(scene);
|
||||
EnsureZoomLayer(scene);
|
||||
Scenes.Add(scene);
|
||||
StagedScene = scene;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using ytLive.Models;
|
||||
using ytLive.Services;
|
||||
using ytLive.Services.Compositor;
|
||||
|
||||
namespace ytLive.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// The zoom box as a real composited layer (TASK 51): right-clicking the live
|
||||
/// preview places a 2x magnifier one z-slot above the background and below every
|
||||
/// other layer. The box drives a transient <see cref="ZoomSceneElement"/> whose
|
||||
/// pixels are the magnified backdrop inside the box; the frame pump composites it
|
||||
/// into the stream/recording like any other layer (output stays the normal tier
|
||||
/// crop — the box is NEVER the crop source). All shapes are centered on the click.
|
||||
/// </summary>
|
||||
public partial class MainViewModel
|
||||
{
|
||||
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;
|
||||
}
|
||||
// Option B anchor: the click point is the CENTER of every shape. Clamped
|
||||
// so the whole box stays inside the master 1920x1080 frame.
|
||||
var x = Math.Clamp(clickX - w / 2, 0, MasterFrameWidth - w);
|
||||
var y = Math.Clamp(clickY - h / 2, 0, MasterFrameHeight - h);
|
||||
_zoomState.X = x;
|
||||
_zoomState.Y = y;
|
||||
_zoomState.Width = w;
|
||||
_zoomState.Height = h;
|
||||
_zoomState.IsActive = true;
|
||||
_zoomState.IsMoving = false;
|
||||
|
||||
var zoomTarget = EnsureZoomLayer(StagedScene);
|
||||
zoomTarget.X = x;
|
||||
zoomTarget.Y = y;
|
||||
zoomTarget.Width = w;
|
||||
zoomTarget.Height = h;
|
||||
zoomTarget.ClipShape = _zoomShape == ZoomShape.Circle ? ClipShape.Round : ClipShape.Traditional;
|
||||
zoomTarget.IsZoomActive = true;
|
||||
zoomTarget.IsVisible = true;
|
||||
ScheduleSave();
|
||||
RefreshZoomUi();
|
||||
}
|
||||
|
||||
public void ToggleZoom()
|
||||
{
|
||||
if (_zoomState.IsActive) DismissZoom();
|
||||
}
|
||||
|
||||
public void DismissZoom()
|
||||
{
|
||||
if (!_zoomState.IsActive) return;
|
||||
_zoomState.IsActive = false;
|
||||
_zoomState.IsMoving = false;
|
||||
var zoomTarget = StagedScene?.Elements.OfType<ZoomSceneElement>().FirstOrDefault();
|
||||
if (zoomTarget != null)
|
||||
{
|
||||
zoomTarget.IsZoomActive = false;
|
||||
zoomTarget.IsVisible = false;
|
||||
}
|
||||
if (StagedScene != null) _sceneGraph.InvalidateBake(StagedScene);
|
||||
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);
|
||||
var zoomTarget = StagedScene?.Elements.OfType<ZoomSceneElement>().FirstOrDefault(e => e.IsZoomActive);
|
||||
if (zoomTarget != null)
|
||||
{
|
||||
zoomTarget.X = _zoomState.X;
|
||||
zoomTarget.Y = _zoomState.Y;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Heals a missing zoom placeholder on every scene after a layout
|
||||
/// load: the invisible 0x0 row parked at z-index 1 (above the background,
|
||||
/// below every other layer), mirroring how backgrounds heal. The zoom box
|
||||
/// itself is transient — the placeholder survives so the layer has a seat.</summary>
|
||||
private void HealZoomLayers()
|
||||
{
|
||||
foreach (var scene in Scenes)
|
||||
EnsureZoomLayer(scene);
|
||||
}
|
||||
|
||||
/// <summary>Gets or inserts the scene's zoom placeholder at z-index 1.</summary>
|
||||
private ZoomSceneElement EnsureZoomLayer(Scene? scene)
|
||||
{
|
||||
var existing = scene?.Elements.OfType<ZoomSceneElement>().FirstOrDefault();
|
||||
if (existing != null) return existing;
|
||||
var zoom = new ZoomSceneElement();
|
||||
_sceneGraph.InsertElement(scene!, Math.Min(1, scene!.Elements.Count), zoom);
|
||||
return zoom;
|
||||
}
|
||||
|
||||
// Single-slot zoom frame cache: the frame-pump resolver runs every tick, so
|
||||
// rebuilding the ~4 MB box raster per tick would churn the LOH even when the
|
||||
// backdrop and box did not change. One active zoom → one cached frame; the
|
||||
// key is the backdrop frames' identity+Epoch and the box rect's ints.
|
||||
private VideoFrame? _zoomFrame;
|
||||
private (byte[]?, long?, byte[]?, long?, int, int, int, int) _zoomFrameKey;
|
||||
|
||||
private VideoFrame? BuildZoomFrame(ZoomSceneElement zoom)
|
||||
{
|
||||
var scene = StagedScene;
|
||||
if (scene == null) return null;
|
||||
|
||||
// Mirror the compositor's backdrop ordering: live capture first, static
|
||||
// background art over it (SceneCompositor.Render, same conditions — live
|
||||
// only when visible, art whenever present).
|
||||
var live = scene.Elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
|
||||
var liveFrame = live is { IsVisible: true } ? ResolveOutputFrameCore(live) : null;
|
||||
var art = scene.Elements.OfType<Source>().FirstOrDefault(s => s.Type == SourceType.Background);
|
||||
var artFrame = art != null ? ResolveOutputFrameCore(art) : null;
|
||||
if (liveFrame == null && artFrame == null) return null;
|
||||
|
||||
var zx = (int)Math.Round(zoom.X);
|
||||
var zy = (int)Math.Round(zoom.Y);
|
||||
var zw = (int)Math.Ceiling(zoom.Width);
|
||||
var zh = (int)Math.Ceiling(zoom.Height);
|
||||
var key = (liveFrame?.BgraPixels, liveFrame?.Epoch, artFrame?.BgraPixels, artFrame?.Epoch, zx, zy, zw, zh);
|
||||
if (key != _zoomFrameKey)
|
||||
{
|
||||
_zoomFrame = ZoomFrameBuilder.Build(
|
||||
liveFrame, artFrame, zx, zy, zw, zh, MasterFrameWidth, MasterFrameHeight);
|
||||
_zoomFrameKey = key;
|
||||
}
|
||||
return _zoomFrame;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pure pixel builder for the zoom layer: samples the box/2 interior
|
||||
/// centred on the box centre from the sequenced backdrop (live capture then art)
|
||||
/// and upscales it 2x — the same region and magnification the preview's
|
||||
/// VisualBrush shows. Static, so the magnification proof is a plain unit test.</summary>
|
||||
internal static class ZoomFrameBuilder
|
||||
{
|
||||
internal static VideoFrame? Build(
|
||||
VideoFrame? liveFrame, VideoFrame? artFrame,
|
||||
int zx, int zy, int zw, int zh,
|
||||
double masterW, double masterH)
|
||||
{
|
||||
if (zw <= 0 || zh <= 0) return null;
|
||||
if (liveFrame == null && artFrame == null) return null;
|
||||
var buffer = new byte[zw * zh * 4];
|
||||
for (var oy = 0; oy < zh; oy++)
|
||||
{
|
||||
// Source window = the box/2 interior CENTERED on the box center (the
|
||||
// same region as the preview's VisualBrush Viewbox), sampled at 2x.
|
||||
var my = zy + zh / 4.0 + (oy + 0.5) * 0.5;
|
||||
var di = oy * zw * 4;
|
||||
for (var ox = 0; ox < zw; ox++, di += 4)
|
||||
{
|
||||
var mx = zx + zw / 4.0 + (ox + 0.5) * 0.5;
|
||||
var b = 0;
|
||||
var g = 0;
|
||||
var r = 0;
|
||||
var a = 0;
|
||||
if (liveFrame != null)
|
||||
Sample(liveFrame, mx, my, masterW, masterH, ref b, ref g, ref r, ref a);
|
||||
if (artFrame != null)
|
||||
{
|
||||
var ab = 0;
|
||||
var ag = 0;
|
||||
var ar = 0;
|
||||
var aa = 0;
|
||||
Sample(artFrame, mx, my, masterW, masterH, ref ab, ref ag, ref ar, ref aa);
|
||||
if (aa >= 255)
|
||||
{
|
||||
b = ab; g = ag; r = ar;
|
||||
}
|
||||
else if (aa > 0)
|
||||
{
|
||||
var inv = 255 - aa;
|
||||
b = (b * inv + ab * aa + 127) / 255;
|
||||
g = (g * inv + ag * aa + 127) / 255;
|
||||
r = (r * inv + ar * aa + 127) / 255;
|
||||
}
|
||||
a = 255;
|
||||
}
|
||||
else if (liveFrame != null)
|
||||
{
|
||||
a = 255;
|
||||
}
|
||||
buffer[di] = (byte)b;
|
||||
buffer[di + 1] = (byte)g;
|
||||
buffer[di + 2] = (byte)r;
|
||||
buffer[di + 3] = (byte)a;
|
||||
}
|
||||
}
|
||||
return new VideoFrame(zw, zh, buffer);
|
||||
}
|
||||
|
||||
/// <summary>Inverse of the compositor's full-frame backdrop blit: maps a master
|
||||
/// pixel back through <see cref="StretchMath.UniformToFill"/> (or the frame's
|
||||
/// CropBounds) into the source canvas and bilinear-samples it. Fixed-point
|
||||
/// bilinear, same precision as SceneCompositor.Bilinear.</summary>
|
||||
private static void Sample(
|
||||
VideoFrame src, double mx, double my, double masterW, double masterH,
|
||||
ref int b, ref int g, ref int r, ref int a)
|
||||
{
|
||||
var cbX = 0;
|
||||
var cbY = 0;
|
||||
var cbW = src.Width;
|
||||
var cbH = src.Height;
|
||||
float scale, ox, oy, drawnW, drawnH;
|
||||
if (src.CropBounds is { } cb && cb.W > 0 && cb.H > 0)
|
||||
{
|
||||
cbX = cb.X;
|
||||
cbY = cb.Y;
|
||||
cbW = cb.W;
|
||||
cbH = cb.H;
|
||||
scale = Math.Max((float)masterW / cbW, (float)masterH / cbH);
|
||||
drawnW = cbW * scale;
|
||||
drawnH = cbH * scale;
|
||||
ox = (float)((masterW - drawnW) / 2);
|
||||
oy = (float)((masterH - drawnH) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
var (uScale, uOx, uOy) = StretchMath.UniformToFill((float)masterW, (float)masterH, src.Width, src.Height);
|
||||
scale = uScale;
|
||||
drawnW = src.Width * uScale;
|
||||
drawnH = src.Height * uScale;
|
||||
ox = uOx;
|
||||
oy = uOy;
|
||||
}
|
||||
|
||||
var sxFloat = cbX + (mx - ox) / drawnW * cbW;
|
||||
var syFloat = cbY + (my - oy) / drawnH * cbH;
|
||||
if (sxFloat < 0f) sxFloat = 0f;
|
||||
if (sxFloat > src.Width - 1f) sxFloat = src.Width - 1f;
|
||||
if (syFloat < 0f) syFloat = 0f;
|
||||
if (syFloat > src.Height - 1f) syFloat = src.Height - 1f;
|
||||
|
||||
var sxi = (int)sxFloat;
|
||||
var syi = (int)syFloat;
|
||||
var sxInv = Math.Min(sxi + 1, src.Width - 1);
|
||||
var syInv = Math.Min(syi + 1, src.Height - 1);
|
||||
var fx8 = (int)((sxFloat - sxi) * 256);
|
||||
var fxInv = 256 - fx8;
|
||||
var fy8 = (int)((syFloat - syi) * 256);
|
||||
var fyInv = 256 - fy8;
|
||||
var stride = src.Width;
|
||||
var p00 = (syi * stride + sxi) * 4;
|
||||
var p10 = (syi * stride + sxInv) * 4;
|
||||
var p01 = (syInv * stride + sxi) * 4;
|
||||
var p11 = (syInv * stride + sxInv) * 4;
|
||||
var px = src.BgraPixels;
|
||||
b = Bilinear(px[p00], px[p10], px[p01], px[p11], fx8, fxInv, fy8, fyInv);
|
||||
g = Bilinear(px[p00 + 1], px[p10 + 1], px[p01 + 1], px[p11 + 1], fx8, fxInv, fy8, fyInv);
|
||||
r = Bilinear(px[p00 + 2], px[p10 + 2], px[p01 + 2], px[p11 + 2], fx8, fxInv, fy8, fyInv);
|
||||
a = Bilinear(px[p00 + 3], px[p10 + 3], px[p01 + 3], px[p11 + 3], fx8, fxInv, fy8, fyInv);
|
||||
}
|
||||
|
||||
private static int Bilinear(int tl, int tr, int bl, int br, int fx8, int fxInv, int fy8, int fyInv)
|
||||
{
|
||||
var top = tl * fxInv + tr * fx8;
|
||||
var bot = bl * fxInv + br * fx8;
|
||||
return (top * fyInv + bot * fy8 + 32768) >> 16;
|
||||
}
|
||||
}
|
||||
+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),
|
||||
|
||||
@@ -12,6 +12,7 @@ areas' docs in their own rows keeps this index small to read.
|
||||
| `MainViewModel.Account.cs` | Session + connected account: `LoadSavedSessionAsync` (DPAPI restore at startup), `SignInAsync`/`ChangeAccountAsync`/`SignOutYouTubeAsync`, `IsConnected`/`AccountAvatarUrl`/`AccountDisplayName`/`AccountStatusLightToolTip` via `SyncConnectedAccount` (cleared on End). |
|
||||
| `MainViewModel.License.cs` | Polar license (freemium): `LicenseKey`/`LicenseMessage`/`IsLicenseValidating`/`IsLicenseEntryOpen`/`IsHubVisible`/`IsPremium`, `OfflineGracePeriod`, `ValidateLicenseAtStartupAsync`/`ActivateLicenseAsync`. |
|
||||
| `MainViewModel.Scenes.cs` | Five-scene catalog: `Scenes` + `StagedScene`/`LiveScene`/`IsEditMode`, `AddScene` (canonical names only), `MissingScenes`/`ShowAddScene`/`AddSceneCommand`; scene wiring `OnScenesChanged`/`WireScene`/`UnwireScene`; transitions `SelectedTransitionType`/`TransitionDurationMs`/`TransitionToScene`/`ApplyTransitionToLive`; global hotkeys `HandleHotkey`/`OpenHotkeyConfig`/`RefreshHotkeyLabels`; scene-gating hints (`CanChangeBackground`). |
|
||||
| `MainViewModel.Zoom.cs` | **Zoom layer (TASK 51)** `ActivateZoom`/`ToggleZoom`/`DismissZoom`/`StartZoomMove`/`UpdateZoomMove`/`EndZoomMove` + `RefreshZoomUi`/`RebuildDimRectsForZoom` — right-click in the live preview parks a **2x magnifier as a real composited layer** at z-index 1 (`ZoomSceneElement`, healed by `HealZoomLayers`/`EnsureZoomLayer` on load + scene create; never persisted). All shapes are CENTERED on the click and clamped to the master frame. `BuildZoomFrame` resolves the backdrop (live capture under static art — same frames/conditions as `SceneCompositor.Render`) and calls the pure static `ZoomFrameBuilder.Build` (box/2 interior centred on the box centre, bilinear, single-slot cache keyed on frame identity+Epoch+box ints so the per-tick resolver allocates nothing while idle). `BuildCompositorOptions` (moved internal = test seam) always returns the tier crop — the box is never the crop source. |
|
||||
| `MainViewModel.Background.cs` | Screen backdrop (Live-only by policy): `ScreenCaptureManager` + `ScreenCaptureSourceFactory` (WinRT GraphicsCapture); background model `EnsureBackground`/`CreateBackground`/`NormalizeBackgrounds`/`HealBackgrounds`/`StampDefaultBackgroundAsset`; screen capture `ReacquireScreenCaptures`/`ResolveAutoCaptureKey`/`RedesignateBackgroundAsync`/`ClearBackgroundCaptureAsync`/`SetBackgroundCapture`/`OnScreenPreviewBitmapChanged`/`OnFullscreenMonitorChanged`/`RefreshBackgroundAutoCapture`/`BackgroundShowDesktop` (keys on full-screen-game monitor or primary display via `Win32FullScreenDetector`); display picker `Displays`/`SetBackgroundDisplayCommand`; preview fns `ActiveBackgroundImage`/`BackgroundImage`/`UpdateActiveBackground`/`UpdateBackgroundImage`/`RefreshSnapshotsAsync`/`LoadBackgroundImage`/`CreatePlaceholderSnapshot`; `ChangeCaptureCommand`/`RefreshCaptureCommand`/`ToggleShowDesktopCommand`/`BrowseBackgroundCommand`. `ShowPreviewPlaceholder` stays in core. |
|
||||
| `MainViewModel.Webcam.cs` | Webcam identity: `_webcam`/`ReacquireWebcam`/`SwapWebcamIdentityAsync`; per-scene configs `AddWebcamToStagedSceneAsync`/`ShowWebcamInStagedScene`/`ChangeWebcamAsync` (Windows picker always opens); capture `OnCameraPreviewBitmapChanged`/`OnCameraFailed`; gating `CanAddWebcam`/`CanChangeWebcam`/`CanShowWebcamInStagedScene`/`IsWebcamSelected`/`WebcamError`; size safeguards `WebcamMax*`/`ClampWebcamToBounds`/`HealLegacySquareRect`; `AddWebcamCommand`/`ChangeWebcamCommand`/`ShowWebcamCommand`. |
|
||||
| `MainViewModel.Audio.cs` | **Mic bar:** read-only realtime meter `AudioLevel`/`MeterFillWidth`/`MeterBrush` (fill = `Math.Min(1, AudioLevelMeter.ToDisplay((float)AudioLevel) * MicVolume)`, green→yellow→red at 60%/80% zones), `MicVolume` (0.8 default; drives `MicMuted` ⇔ volume 0; unmute flash `BeginVolumeFlash`/`EndVolumeFlash`/`CancelVolumeFlash`), `MicSourceName` (left-justified inside the bar, persists via `LayoutStore.Settings`), `ToggleMicMute`/`PickMicrophone` (`MicPickerDialog`; picked device swaps the live source via `_audioMixer.RestartMic()`, loopback keeps running), status dot `MicStatus`/`MicStatusBrush`/`MicStatusToolTip` (`Models.MicStatus`: green Connected / yellow Problem / red no-device), capture starts once at startup (`StartMicCaptureAsync`) — NOT go-live. **Game audio bar:** overlaid bottom-center, always visible (show/hide gating removed 2026-08-15); `GameAudioLevel` (loopback via `OnLoopbackLevelChanged`), `GameAudioVolume`/`GameMuted`/`GameMuteText`/`ToggleGameMute`/`PushSystemVolume`, mirror volume flash `SetGameVolumeAdjusting`/`BeginGameVolumeFlash`/`EndGameVolumeFlash`/`CancelGameVolumeFlash`. Mixer handlers `OnMicLevelChanged`/`OnLoopbackLevelChanged`/`OnMicConnected`/`OnMicFailed`. |
|
||||
|
||||
Reference in New Issue
Block a user