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; } }