using System;
using System.Linq;
using System.Windows;
using ytLive.Models;
namespace ytLive.ViewModels;
///
/// The zoom box (creator-corrected model, 2026-10-08): right-clicking the live
/// preview anchors a CAPTURE window — the box/2 region centred on the click — and
/// parks a movable 2x magnifier box on screen. The box shows a live, always-on,
/// magnified view of that anchored capture region (the CLICKED area, never re-follows
/// the box); dragging it only repositions the box. The content is composited LAST
/// (layer n+1) by from the already-
/// composed frame, so it captures every layer — prove it with the ZoomMagnifierTests
/// banded-buffer "box over blue shows green" proof. The transient
/// carries the box placement + capture anchor; the
/// output crop stays the normal tier (the box is NEVER the crop source).
///
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:
// HorizontalRectangle: 4:3 per creator ("more of a 640x480 aspect
// ratio") — NOT the 16:9 of the first cut, which read as a square.
h = maxH;
w = h * 4.0 / 3.0;
break;
}
// The box AND the capture are both centred on the click; the capture window
// is the box/2 interior and is FIXED after this call. Each is clamped to the
// master 1920x1080 frame independently (an edge click can push the capture
// toward centre while the box still sits right at the edge).
var x = Math.Clamp(clickX - w / 2, 0, MasterFrameWidth - w);
var y = Math.Clamp(clickY - h / 2, 0, MasterFrameHeight - h);
var capX = Math.Clamp(clickX - w / 4, 0, MasterFrameWidth - w / 2);
var capY = Math.Clamp(clickY - h / 4, 0, MasterFrameHeight - h / 2);
_zoomState.X = x;
_zoomState.Y = y;
_zoomState.Width = w;
_zoomState.Height = h;
_zoomState.CaptureX = capX;
_zoomState.CaptureY = capY;
_zoomState.IsActive = true;
_zoomState.IsMoving = false;
var zoomTarget = EnsureZoomLayer(StagedScene);
zoomTarget.X = x;
zoomTarget.Y = y;
zoomTarget.Width = w;
zoomTarget.Height = h;
zoomTarget.CaptureX = capX;
zoomTarget.CaptureY = capY;
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().FirstOrDefault();
if (zoomTarget != null)
{
zoomTarget.IsZoomActive = false;
zoomTarget.IsVisible = false;
zoomTarget.CaptureX = 0;
zoomTarget.CaptureY = 0;
}
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().FirstOrDefault(e => e.IsZoomActive);
if (zoomTarget != null)
{
zoomTarget.X = _zoomState.X;
zoomTarget.Y = _zoomState.Y;
// CaptureX/Y are deliberately NOT touched: dragging relocates the box,
// never the anchored captured area (creator ruling 2026-10-08).
}
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();
}
/// Heals a missing zoom placeholder on every scene after a layout
/// load: the invisible 0x0 row mirrored by the healed backgrounds. The zoom box
/// itself is transient — the placeholder survives so the layer has a seat. The
/// compositor ranks the ACTIVE box last regardless of this index.
private void HealZoomLayers()
{
foreach (var scene in Scenes)
EnsureZoomLayer(scene);
}
/// Gets or inserts the scene's zoom placeholder at z-index 1.
private ZoomSceneElement EnsureZoomLayer(Scene? scene)
{
var existing = scene?.Elements.OfType().FirstOrDefault();
if (existing != null) return existing;
var zoom = new ZoomSceneElement();
_sceneGraph.InsertElement(scene!, Math.Min(1, scene!.Elements.Count), zoom);
return zoom;
}
}