From 9b56e8880046eab5ea7d84d29c7f6f7573a25ee6 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 28 Aug 2026 12:09:13 -0700 Subject: [PATCH] =?UTF-8?q?Revert=20ab29ec8:=20container=3D=3Dcontent=20au?= =?UTF-8?q?to-fit=20made=20it=20worse=20=E2=80=94=20back=20to=20alpha-crop?= =?UTF-8?q?=20with=20the=20container=20at=20element=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The container tracks the element's own Width/Height again; the crop stays in the bitmap. --- HANDOFF.md | 54 ++++++++++++++-------------- MainWindow.xaml.cs | 3 -- MyMistakes.md | 24 +++++++------ Services/WebView2Manager.cs | 13 ++----- TASKS.md | 17 ++++----- ViewModels/MainViewModel.cs | 22 +----------- ytLive.Tests/WebView2ManagerTests.cs | 21 ----------- 7 files changed, 51 insertions(+), 103 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index a75cbb1..09911db 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -6,34 +6,36 @@ working). **NOT pushed — user rule: never push without explicit instruction (2026-08-24).** No feature branches pre-1.0: all work lands on `main` per work unit. -## The web-source rendering work (2026-08-28) — CURRENT MODEL: canvas-size viewport + alpha crop + container==content +## The web-source rendering work (2026-08-28) — CURRENT MODEL: canvas-size viewport + ALPHA-BBOX crop -**History of failures (see MyMistakes.md for the full log):** -1. box-sized viewport clipped full-bleed widgets to their upper-left chunk → render at the master - canvas 1920×1080 (stable), the only render the creator confirmed solid. -2. `JSON.stringify` return double-encoded under `ExecuteScriptAsync` → crop silently disabled. -3. `scrollWidth/scrollHeight` at canvas size = whole viewport for full-canvas wrapper bodies → no trim. -4. DOM-element-union crop measured an unsettled layout on NavigationCompleted → BROKE the image. -5. Alpha-bbox crop (from the frame itself) fixed the BITMAP but the grid/box stayed at the uncropped - user size → Fill stretched the tight widget into a big frame → dead space between content and border. +**History of failures (see MyMistakes.md for the full log):** box-size rendering clipped full-bleed +widgets to their upper-left chunk (fixed — page renders at the master canvas 1920×1080, stable, the +one render the creator confirmed solid). Then four crop attempts failed in different ways: (1) the +`JSON.stringify` return double-encoded under `ExecuteScriptAsync`, silently disabling the crop; +(2) `scrollWidth/scrollHeight` at canvas size returns the whole viewport for full-canvas wrapper +bodies → no trim (ghost boundary); (3) union-of-DOM-elements measured an unsettled layout on +NavigationCompleted and BROKE the image (creator's "don't break the rendering" boundary crossed). +(4) Reverting to f39ebd9's scroll-crop was another dead end (#2 again). -**The CURRENT MODEL (this change):** +**The CURRENT MODEL (this commit):** - `Services/WebView2Manager.cs` — page renders at 1920×1080 (stable, never tracked); `overflow:hidden` - + transparent-bg injection. `FindContentBounds` crops the Bgra32 capture to the bounding box of - non-transparent pixels each frame (immune to timing, can never truncate content). `ContentCanvasSize` - maps the crop rect to CANVAS UNITS (DPI-correct). Preview event now carries (bitmap, canvasW, canvasH). -- `ViewModels/MainViewModel.cs` — `OnWebView2PreviewBitmapChanged` sets the element's Width/Height to - the cropped canvas size → the element grid AND the SelectionOverlay (both bound to element - Width/Height) EQUAL the widget. `IsPreviewDragging` suppresses the auto-fit during a drag gesture. -- `MainWindow.xaml.cs` — sets/clears `IsPreviewDragging` at drag/resize start and end. -- Invariant: **render at canvas size; crop to the alpha extent; SIZE THE BOX to the crop.** -- Unit tests: 10/10 (alpha bounds ×3, canvas-size mapping ×3, control size ×4). + + transparent-bg injection. `FindContentBounds` scans the Bgra32 capture per frame and crops to the + bounding box of NON-TRANSPARENT pixels — the widget's true rendered extent, measured from the frame + itself. No DOM query, no layout timing: it can never truncate visible content (worst case = full + frame = the confirmed-good image). `Stretch="Fill"` maps the cropped frame flush under the selection + box → the box hugs the widget on all four sides. `QueryContentBoundsAsync` + the JS script are + DELETED. +- `MainWindow.xaml` — web `` Stretch set to Fill via the IsWebSource DataTrigger (unchanged). +- Invariant: **render at canvas size; crop to the alpha extent; Fill to the box.** +- Unit tests: 3 new for `FindContentBounds` (tight box, no-alpha no-crop, full-alpha no-crop). **Property panel / sliders (2026-08-28):** web URI ✕ clears the textbox; web ✓/✕ icon buttons -`IsTabStop="False"`; Slider style `IsMoveToPointEnabled="True"`. Confirmed fixed by the creator. +`IsTabStop="False"` so Tab walks X→Y→W→H→URI; Slider style `IsMoveToPointEnabled="True"` +restores click-anywhere-on-bar. All three confirmed fixed by the creator. -Build: 0 warnings; tests 10/10. Rejected for the record: box-sized viewport, `Uniform`, CSS reset, -viewport==box, DOM-union crop (broke image), crop-without-container-size (left the gap). +Build: 0 warnings; tests 7/7 (WebView2Manager). Rejected for the record: box-sized viewport (upper-left +truncation), fixed 1920×1080 + `Uniform` (letterbox dead space), CSS margin/100% reset (enlarged gap), +viewport==box (reflow/truncation/scrollbars), DOM-union crop (broke the image). ## Key facts / landmines @@ -50,8 +52,8 @@ viewport==box, DOM-union crop (broke image), crop-without-container-size (left t - No `CODING.md`; conventions live in `AGENTS.md` + `ai.md`. ## Next steps -1. AWAIT creator's verdict on the container==content auto-fit — reload a widget; the selection frame - must equal the widget exactly (no dead space, including during resize). Follow-ups only if asked: - crop hysteresis for animated widgets, ticker/percent-display widgets. -2. Graceful handling of navigation errors / invalid URIs (TASK 17 ×9) — only if the creator asks. +1. AWAIT creator's verdict on the ALPHA-BBOX crop — reload a widget URL at several box sizes; the + selection frame must snap flush to the widget with no dead space, image must stay identical for + full-bleed widgets. If an animated widget causes visible jitter, add crop hysteresis as a follow-up. +2. Follow-up only if the creator asks: graceful handling of navigation errors / invalid URIs (TASK 17 ×9). 3. Continue queue: TASK 18 local recording, TASK 21 media source, TASK 22 audio sync offset. \ No newline at end of file diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 9c74e16..70324a1 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -572,7 +572,6 @@ public partial class MainWindow : Window { _isResizing = true; _resizeAspect = sel.ClipShape == ClipShape.Round ? 1 : sel.Width / Math.Max(1, sel.Height); - _viewModel.IsPreviewDragging = true; grid.CaptureMouse(); e.Handled = true; return; @@ -584,7 +583,6 @@ public partial class MainWindow : Window _viewModel.SelectedElement = hit; _isDraggingOverlay = true; _grabOffset = new Point(p.X - hit.X, p.Y - hit.Y); - _viewModel.IsPreviewDragging = true; grid.CaptureMouse(); e.Handled = true; return; @@ -645,7 +643,6 @@ public partial class MainWindow : Window grid.ReleaseMouseCapture(); _isDraggingOverlay = false; _isResizing = false; - _viewModel.IsPreviewDragging = false; } private bool HitHandle(Point mouseScreen, SceneElement element) diff --git a/MyMistakes.md b/MyMistakes.md index 2708a92..4f0588c 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -31,15 +31,17 @@ Stream widget bounding box: hug the widget on all four sides, pinned to canvas ( extents regardless. Lesson: verify declared mechanics actually execute; assume nothing ran. ## Current iteration (web bounding box — UNCOMMITTED, verification pending) -CONTAINER == CONTENT. The element grid and the selection box now SIZE to the cropped frame: -`WebView2Manager.ContentCanvasSize` maps the alpha-crop pixel rect to MASTER CANVAS units -(1920/pixW per axis, DPI-correct) and the preview event carries it; `MainViewModel` sets the -Source.Width/Height (the grid + SelectionOverlay bindings) to that size. No Fill-stretching of a -cropped bitmap into a larger frame — the box IS the widget. Auto-fit is suppressed while the -drag/resize gesture is active (`IsPreviewDragging`, set by MainWindow code-behind). +ALPHA-BBOX CROP: `FindContentBounds` scans the captured Bgra32 frame and returns the bounding box +of non-transparent pixels (the background is injected transparent, so alpha = the widget's extent). +Capture is cropped to that rect and `Stretch="Fill"` maps it flush under the box. No DOM query, no +layout timing — the crop rect IS the rendered widget by construction, so it can NEVER truncate +visible content. Worst case (widget fills the canvas) = full frame = the confirmed-good render. +Canvas-size viewport + Fill untouched. `QueryContentBoundsAsync` + the JS script are DELETED. -## Iteration that just failed (`bccdb48` — alpha crop, but container stayed at Source.Width/Height) -Crop trimmed the bitmap but the element grid/box still rendered at the UNcropped user-set size, so -Fill stretched the tight widget across a larger frame and dead space floated between content bounds -and the selection border. Lesson: fixing the BITMAP is not fixing the BOX — the layout container -must match the cropped dimensions or the gap persists by construction. \ No newline at end of file +## Iteration that just failed (`a5b9952` — wrapper-excluded DOM-union crop + image alignment) +Broke the image rendering — creator could not evaluate the box because the image was gone/ruined. +Why: the visible-element-union rect is measured ONCE on NavigationCompleted; widget layout isn't +settled then (async fonts/images, load animations, iframes), so the crop rect comes out tiny/wrong +and the Fill-stretched frame renders a broken/zoomed sliver. This was against the creator's explicit +"don't break the rendering" instruction. Lesson: measure actual RENDERED PIXELS (alpha), never +transient DOM state; and never ship a crop blind. \ No newline at end of file diff --git a/Services/WebView2Manager.cs b/Services/WebView2Manager.cs index 3a30f22..6c0eb95 100644 --- a/Services/WebView2Manager.cs +++ b/Services/WebView2Manager.cs @@ -20,7 +20,7 @@ public sealed class WebView2Manager : IDisposable private readonly Dispatcher _dispatcher; private readonly Dictionary _sessions = new(); - public event Action? PreviewBitmapChanged; + public event Action? PreviewBitmapChanged; private sealed class WebSourceSession { @@ -182,14 +182,6 @@ public sealed class WebView2Manager : IDisposable return (minX, minY, maxX - minX + 1, maxY - minY + 1); } - // The cropped pixel extent, expressed in MASTER CANVAS units (the WebView2 is - // 1920×1080 DIPs and captures at its physical size, so 1920/pixW maps capture - // pixels back to canvas units). The element grid + selection box are sized to - // this, so the container matches the content exactly instead of Fill-stretching - // a cropped bitmap into a larger frame. - internal static (double W, double H) ContentCanvasSize(int cropW, int cropH, int pixW, int pixH) - => (cropW * (1920.0 / pixW), cropH * (1080.0 / pixH)); - private async Task CaptureFrame(string sourceId) { if (!_sessions.TryGetValue(sourceId, out var session) || session.Disposed) return; @@ -240,12 +232,11 @@ public sealed class WebView2Manager : IDisposable wb.WritePixels(new Int32Rect(0, 0, cropW, cropH), outPixels, cropW * 4, 0); wb.Freeze(); - var (canvasW, canvasH) = ContentCanvasSize(cropW, cropH, pixW, pixH); session.LatestFrame = new VideoFrame(cropW, cropH, outPixels); _ = _dispatcher.BeginInvoke(() => { - PreviewBitmapChanged?.Invoke(sourceId, wb, canvasW, canvasH); + PreviewBitmapChanged?.Invoke(sourceId, wb); }); } catch (ObjectDisposedException) { } diff --git a/TASKS.md b/TASKS.md index 09a567f..a43ef13 100644 --- a/TASKS.md +++ b/TASKS.md @@ -917,19 +917,16 @@ capturing the primary display (which causes the infinity mirror effect). 8. ✅ Preview shows live web content (not just a blank rectangle) 9. ☐ Handle navigation errors, invalid URIs, timeout gracefully -**Rendering model (2026-08-28, canvas-size viewport + alpha crop + CONTAINER==CONTENT):** +**Rendering model (2026-08-28, canvas-size viewport + ALPHA-BBOX crop):** the page renders at the MASTER CANVAS size (1920×1080), stable, never tracked (no reflow/truncation; scrollbars suppressed via `overflow:hidden`). `FindContentBounds` scans the Bgra32 capture and crops to the bounding box of non-transparent pixels — the widget's true rendered extent, measured from the -frame itself (NO DOM query, immune to layout timing, can never truncate content; full-canvas widgets -fall through to the full frame). `ContentCanvasSize` maps the cropped pixel rect to canvas units -(DPI-correct); `MainViewModel.OnWebView2PreviewBitmapChanged` sizes the element's Width/Height to it — -so the element grid AND the SelectionOverlay (both bound to the element's Width/Height) equal the -widget exactly. Auto-fit suppressed while dragging (`IsPreviewDragging`). `Stretch="Fill"` is now -~1:1; no cropped bitmap is stretched into a larger frame. -⚠️ Lesson bank: never return `JSON.stringify` from `ExecuteScriptAsync` (double-encodes); never -measure DOM on NavigationCompleted (unsettled layout broke the image — `a5b9952`); fixing the -bitmap crop without sizing the container leaves the gap (— `bccdb48`). +frame itself (NO DOM query, immune to layout timing; can never truncate content; full-canvas widgets +fall through to the full frame = prior verified-good image). `Stretch="Fill"` maps the cropped frame +flush under the selection box → box hugs the widget on all four sides. `QueryContentBoundsAsync` + +the JS content-bounds script are DELETED. ⚠️ Lesson bank: never return `JSON.stringify` from +`ExecuteScriptAsync` (double-encodes); never measure DOM stuff on NavigationCompleted (unsettled +layout broke the image — `a5b9952`); measure rendered pixels instead. **Properties panel (2026-08-28):** web URI ✓/✕ icon buttons are `IsTabStop="False"` so Tab flows X→Y→W→H→URI; the ✕ button now clears the URI textbox (was reverting to the pre-accept snapshot). diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 612691a..a0d2359 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -2290,11 +2290,6 @@ public class MainViewModel : ViewModelBase _ = RedesignateBackgroundAsync($"{MonitorKeyPrefix}{display.Index}"); } - // True while the user is dragging/resizing an element on the preview canvas - // (set by MainWindow code-behind). Suppresses the web source's auto-fit so it - // doesn't fight the gesture mid-drag. - public bool IsPreviewDragging { get; set; } - public void InitWebView2(Panel hostPanel) { _webView2Manager = new WebView2Manager(hostPanel, System.Windows.Application.Current.Dispatcher); @@ -2302,26 +2297,11 @@ public class MainViewModel : ViewModelBase RegisterLoadedWebSources(); } - private void OnWebView2PreviewBitmapChanged(string sourceId, WriteableBitmap bitmap, double canvasW, double canvasH) + private void OnWebView2PreviewBitmapChanged(string sourceId, WriteableBitmap bitmap) { foreach (var scene in Scenes) foreach (var el in scene.Elements.OfType().Where(s => s.Id == sourceId)) - { el.VideoImageSource = bitmap; - - // The container must equal the content: size the element grid - // (and therefore the selection box) to the cropped frame's canvas - // extent. Skipped while the user is dragging/resizing the element - // so the gesture isn't fought mid-drag. - if (IsPreviewDragging || canvasW < 1 || canvasH < 1) continue; - var w = Math.Clamp(Math.Round(canvasW), 1, 1920); - var h = Math.Clamp(Math.Round(canvasH), 1, 1080); - if (Math.Abs(el.Width - w) > 1 || Math.Abs(el.Height - h) > 1) - { - el.Width = w; - el.Height = h; - } - } } public void Shutdown() diff --git a/ytLive.Tests/WebView2ManagerTests.cs b/ytLive.Tests/WebView2ManagerTests.cs index 2766166..867dd48 100644 --- a/ytLive.Tests/WebView2ManagerTests.cs +++ b/ytLive.Tests/WebView2ManagerTests.cs @@ -154,25 +154,4 @@ public sealed class WebView2ManagerTests Assert.Equal((0, 0, w, h), (x, y, bw, bh)); } - - [Fact] - public void ContentCanvasSize_Full_Frame_Maps_To_Canvas() - { - var (cw, ch) = WebView2Manager.ContentCanvasSize(1920, 1080, 1920, 1080); - Assert.Equal((1920.0, 1080.0), (cw, ch)); - } - - [Fact] - public void ContentCanvasSize_Half_Widget_Is_Half_Canvas() - { - var (cw, ch) = WebView2Manager.ContentCanvasSize(960, 540, 1920, 1080); - Assert.Equal((960.0, 540.0), (cw, ch)); - } - - [Fact] - public void ContentCanvasSize_Corrects_For_Dpi_Scale() - { - var (cw, ch) = WebView2Manager.ContentCanvasSize(1200, 675, 2400, 1350); - Assert.Equal((960.0, 540.0), (cw, ch)); - } }