diff --git a/HANDOFF.md b/HANDOFF.md index e9e1e09..14aafeb 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -6,34 +6,26 @@ 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: viewport == element box +## The web-source rendering work (2026-08-28) — CURRENT MODEL: stable viewport + Fill -**Background:** the fixed-viewport fix (1920×1080 control + `Stretch="Uniform"`) made the page -render without truncation but left dead space between the selection box and the content (box -aspect ≠ capture aspect → letterbox; box is sized by the parent layout, capture is a separate -hardcoded viewport — they bind to different rects). A CSS-reset experiment made the gap worse -and was shelved by the creator. He stressed the systems must be GEOMETRY-AGNOSTIC: no 16:9, -no fixed render size, no per-widget assumptions. +**Background:** viewport==box made dragging the box reflow the page (truncation, scrollbars, +unrecoverable image, "janky at stressful geometries"). Fixed-viewport + `Uniform` left letterbox +dead space. CSS reset enlarged the gap. Scrollbars were called "super fail". -**The model that delivers it (implemented, needs visual verification):** -- `Services/WebView2Manager.cs` — the WebView2 viewport IS the element box - (`Source.Width × Source.Height`). The page renders at exactly the box size; a `PropertyChanged` - hook keeps the viewport locked to the box (re-triggers capture on resize, sub-0.5px drift is - ignored). `CaptureFrame` is unchanged (aspect-preserving Physical-size capture). -- `MainWindow.xaml` — web `` uses `Stretch="Fill"` (capture aspect == box aspect by - construction, so Fill == Uniform with zero dead space; during a transient stale frame it just - holds aspect briefly). Everything else still `UniformToFill`. -- Selection rect == content rect by construction → flush edges, no padding, works for any widget - geometry the creator throws at it. FramePump still reads `GetLatestFrame` via `ResolveOutputFrame`. -- Transparent-background JS injection retained (background only — no layout/CSS-reset meddling). +**The model that satisfies invariants (implemented, needs visual verification):** +- `Services/WebView2Manager.cs` — the page renders ONCE at the viewport size the element was + created at (snapshot in `Register`, never tracked → no reflow, no truncation during resize). + `overflow:hidden` (html+body) + transparent-bg injection on every nav — nothing else, no + layout/CSS meddling. Immediate capture on `NavigationCompleted`. `FramePump` unchanged. +- `MainWindow.xaml` — web `` uses `Stretch="Fill"`: any box the user draws is covered + edge-to-edge, zero dead space, zero crop (OBS free-transform: fixed browser resolution, + scene-item scales the texture). Aspect distortion only when the box aspect ≠ viewport aspect. +- Invariants: **page viewport is stable; display Fill-scales texture to the box.** -Build: 0 warnings; WebView2Manager tests 4/4 (size tracks source, sub-pixel drift ignored, -minimum 1, transparent bg). +Build: 0 warnings; tests 4/4 (snapshot at register, no tracking after, min 1, transparent bg). -**Landmines (earlier attempts — do not repeat):** fixed-viewport (letterbox dead space), -CSS reset/margin juggling (enlarged the gap), control-tracking-box combined with -`UniformToFill` display (truncation), ZoomFactor meddling. The working invariant is: -**viewport rect == selection rect, display = Fill.** +**Rejected for the record:** fixed 1920×1080+`Uniform` (letterbox), CSS margin/100% reset +(enlarged gap), viewport==box (reflow/truncation/scrollbars). ## Key facts / landmines diff --git a/Services/WebView2Manager.cs b/Services/WebView2Manager.cs index 3e1dbe0..1fbea93 100644 --- a/Services/WebView2Manager.cs +++ b/Services/WebView2Manager.cs @@ -40,10 +40,12 @@ public sealed class WebView2Manager : IDisposable { if (_sessions.ContainsKey(source.Id)) return; - // Geometry-agnostic OBS model: the browser viewport IS the element box. - // The page renders at exactly Source.Width × Source.Height, so the - // captured frame and the selection rect are always the same rectangle — - // no fixed render size, no aspect assumptions, no dead space or padding. + // OBS model: the page renders ONCE at a stable viewport — the size the + // element was created at (snapshot, never tracked, so dragging the box + // never reflows/truncates the page and no scrollbars appear). The + // captured texture is then stretched by the display (Stretch=Fill) into + // whatever box the user draws, so the frame always bounds content with + // zero dead space. Geometry-agnostic: any widget, any box. var webView = new WebView2 { Width = Math.Max(1, source.Width), @@ -69,22 +71,6 @@ public sealed class WebView2Manager : IDisposable _sessions[source.Id] = session; - // Interop sync: keep the viewport locked to the element box so the - // selection rect always bounds the actual rendered content. - source.PropertyChanged += (_, e) => - { - if (e.PropertyName is not (nameof(Source.Width) or nameof(Source.Height))) return; - if (session.Disposed) return; - if (Math.Abs(session.Control.Width - source.Width) > 0.5 || - Math.Abs(session.Control.Height - source.Height) > 0.5) - { - session.Control.Width = Math.Max(1, source.Width); - session.Control.Height = Math.Max(1, source.Height); - if (session.Initialized) - _ = CaptureFrame(source.Id); - } - }; - timer.Tick += (_, _) => { if (!session.Disposed && session.Initialized) @@ -129,14 +115,18 @@ public sealed class WebView2Manager : IDisposable { _ = cws.ExecuteScriptAsync( "document.documentElement.style.background='transparent';" + - "document.body.style.background='transparent';"); + "document.documentElement.style.overflow='hidden';" + + "document.body.style.background='transparent';" + + "document.body.style.overflow='hidden';"); }; cws.NavigationCompleted += (_, _) => { _ = cws.ExecuteScriptAsync( "document.documentElement.style.background='transparent';" + - "document.body.style.background='transparent';"); + "document.documentElement.style.overflow='hidden';" + + "document.body.style.background='transparent';" + + "document.body.style.overflow='hidden';"); _ = CaptureFrame(source.Id); }; diff --git a/TASKS.md b/TASKS.md index b068c6a..d41704f 100644 --- a/TASKS.md +++ b/TASKS.md @@ -917,15 +917,17 @@ 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, geometry-agnostic — supersedes the earlier fixed-viewport model):** -the WebView2 viewport IS the element box (`Source.Width × Source.Height`); the page renders at -exactly the box size and the captured frame is displayed with `Stretch="Fill"`, so the selection -rect always bounds the actual rendered content — no fixed render size, no aspect assumptions, -no dead space/padding for any widget geometry. A `PropertyChanged` hook keeps the viewport -locked to the box; capture is re-triggered on resize. Graceful-error handling (item 9) is a -follow-up. Earlier attempts for the record: (a) fixed 1920×1080 viewport + `Stretch="Uniform"` -fixed the truncation but left box-vs-content gaps (box aspect ≠ capture aspect); (b) CSS reset -injection made gaps larger (belayed by creator). +**Rendering model (2026-08-28, stable-viewport + fill — the working invariant):** +the page renders ONCE at the size the element was created at (snapshot in `Register`, never +tracked) — so dragging the box never reflows the page, never truncates it, and scrollbars are +suppressed (`overflow:hidden` on html/body injection; background-transparent only otherwise, +no layout meddling). The captured texture is displayed with `Stretch="Fill"`, so any box the +user draws is covered edge-to-edge with zero dead space (OBS free-transform behavior: +browser renders at a fixed resolution, the scene-item scales the texture). The invariants: +the page viewport is stable; the display Fill-scales it to the box. Graceful-error handling +(item 9) is a follow-up. Attempts rejected for the record: fixed 1920×1080 + `Uniform` +(letterbox dead space), CSS margin/100% reset (enlarged the gap), viewport==box (page reflow +→ truncation/scrollbars/jank). ### Design decisions diff --git a/ytLive.Tests/WebView2ManagerTests.cs b/ytLive.Tests/WebView2ManagerTests.cs index 18f6158..018fd98 100644 --- a/ytLive.Tests/WebView2ManagerTests.cs +++ b/ytLive.Tests/WebView2ManagerTests.cs @@ -20,7 +20,7 @@ public sealed class WebView2ManagerTests } [Fact] - public void WebView2_Control_Size_Tracks_Source_Dimensions() + public void WebView2_Control_Size_Snapshots_Source_At_Register() { _app.Run(() => { @@ -38,17 +38,11 @@ public sealed class WebView2ManagerTests var webView = panel.Children.OfType().First(); Assert.Equal(640.0, webView.Width); Assert.Equal(480.0, webView.Height); - - source.Width = 1024; - source.Height = 768; - - Assert.Equal(1024.0, webView.Width); - Assert.Equal(768.0, webView.Height); }); } [Fact] - public void WebView2_Control_Size_Ignores_Sub_One_Pixel_Drift() + public void WebView2_Control_Size_Does_Not_Track_Box_After_Register() { _app.Run(() => { @@ -65,10 +59,10 @@ public sealed class WebView2ManagerTests var webView = panel.Children.OfType().First(); - source.Width = 640.4; - Assert.Equal(640.0, webView.Width); + source.Width = 1024; + source.Height = 768; - source.Height = 480.4; + Assert.Equal(640.0, webView.Width); Assert.Equal(480.0, webView.Height); }); }