diff --git a/MainWindow.xaml b/MainWindow.xaml index 8a46848..db18f9f 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -857,6 +857,12 @@ + + + + diff --git a/MyMistakes.md b/MyMistakes.md index 5de9841..77e84bb 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -30,15 +30,19 @@ Stream widget bounding box: hug the widget on all four sides, pinned to canvas ( wasn't 0,0. Same silent JSON bug — the crop still never executed — and the measure was scroll extents regardless. Lesson: verify declared mechanics actually execute; assume nothing ran. -## Current iteration (web bounding box — committed, NOT pushed, verification pending) -- Render stays the confirmed-solid 1920×1080 canvas-size viewport + `Stretch="Fill"` (UNTOUCHED). -- Crop BEHAVIOR restored to f39ebd9 exactly: capture cropped from (0,0) to - `scrollWidth/scrollHeight` extents — the code that nailed right/bottom. Real-object return - keeps the crop actually executing (past cause: JSON.stringify double-encode silently disabled it). +## Current iteration (web bounding box — UNCOMMITTED, verification pending) +Render stays the confirmed-solid 1920×1080 canvas-size viewport + `Stretch="Fill"` (UNTOUCHED). +Crop now uses the WIDGET rect, not scroll extents: union of every visible element's +`getBoundingClientRect`, EXCLUDING elements spanning ≥98% of the viewport (bodies, +full-canvas background layers — they are wrappers, not content). Crop is taken AT the +rect's top-left (l,t) so content pins to bitmap (0,0). Web Image explicitly +`Margin="0"` + `HorizontalAlignment="Left"`/`VerticalAlignment="Top"`. -## Iteration that just failed (`582d1f4` — union of every `body *` element rect) -Gap moved to ALL sides. The union includes invisible full-canvas layout wrappers, so the crop rect -came out larger than the visible widget; the offset crop (+ Rewatched fills from a subset of the -canvas. Over-engineering: the fix that already worked was f39ebd9's simple scroll-extents crop. -Creator's instruction: go back to that code. DONE — do not re-introduce element-union measuring. -Lesson: when a prior iteration is declared good, REVERT TO IT; do not build a cleverer replacement. \ No newline at end of file +## Iteration that just failed (`b862a0f` — reverted to f39ebd9's scrollWidth/Height crop) +Creator still saw a massive "ghost" frame around the visible widget. Root cause: for widgets +whose body/background spans the full 1920×1080 canvas, `scrollWidth/scrollHeight` returns the +WHOLE viewport → the crop keeps massive transparent margins → dead space inside the bitmap → +content floats inside the Fill-stretched box. So the earlier "nailed right/bottom" (f39ebd9) was +only right for box-sized viewports: at the canvas size, scroll extents == viewport for full-canvas +wrappers. Lesson: never trust scrollWidth/scrollHeight at canvas-size render; measure visible +content and exclude full-viewport wrappers/backgrounds. \ No newline at end of file diff --git a/Services/WebView2Manager.cs b/Services/WebView2Manager.cs index 9a60464..f3854d7 100644 --- a/Services/WebView2Manager.cs +++ b/Services/WebView2Manager.cs @@ -29,6 +29,8 @@ public sealed class WebView2Manager : IDisposable public VideoFrame? LatestFrame; public double ViewportCssW; public double ViewportCssH; + public double ContentCssX; + public double ContentCssY; public double ContentCssW; public double ContentCssH; public bool Disposed; @@ -159,19 +161,48 @@ public sealed class WebView2Manager : IDisposable catch (Exception ex) { AppLog.Write($"WebView2Manager: navigate failed: {ex.Message}"); } } - // The page's real content rect (scrollWidth/Height) can be smaller than the - // viewport for widgets that don't fill it — the leftover area is dead space - // that must not stretch across the selection box. Crop the capture to the - // content bounds (anchored at 0,0) so the box shows exactly the widget. - // This restores f39ebd9's measurement (scrollWidth/Height), which the - // creator confirmed nailed the right and bottom of the box. The result is - // returned as a real JS object, NOT JSON.stringify — ExecuteScriptAsync + // The true widget rect for the crop. scrollWidth/scrollHeight is useless for + // widgets whose body/background spans the full 1920×1080 canvas — it returns + // the whole viewport, so the crop keeps massive transparent margins and the + // rendered bitmap floats inside the Fill-stretched box ("ghost boundary"). + // Fix: union the getBoundingClientRect of every visible element, EXCLUDING + // elements that span (nearly) the full viewport — bodies and full-canvas + // background/overlay layers are layout wrappers, not widget content. If the + // exclusion empties the set, the widget IS full-bleed: use the full rect. + // Returned as a real JS object (NOT JSON.stringify — ExecuteScriptAsync // JSON-encodes the result, so stringifying would double-encode and the - // JSON parse would throw, silently disabling the crop. + // parse would throw, silently disabling the crop). private const string ContentBoundsScript = - "({w:document.documentElement.scrollWidth||document.body.scrollWidth||0," + - "h:document.documentElement.scrollHeight||document.body.scrollHeight||0," + - "vw:window.innerWidth||1,vh:window.innerHeight||1})"; + "(function(){" + + "var vw=window.innerWidth||1,vh=window.innerHeight||1;" + + "var all=[];var body=document.body;" + + "if(body){var br=body.getBoundingClientRect();if(br.width||br.height)all.push(br);" + + "var els=body.querySelectorAll('*');" + + "for(var i=0;i=vw*0.98&&rr.height>=vh*0.98)continue;" + + "content.push(rr);" + + "}" + + "var use=content.length?content:all;" + + "var l=use[0].left,t=use[0].top,rt=use[0].right,bt=use[0].bottom;" + + "for(var k=1;k= 1 && session.ViewportCssH >= 1 && session.ContentCssW >= 1 && session.ContentCssH >= 1) { var sx = pixW / session.ViewportCssW; var sy = pixH / session.ViewportCssH; - cropW = (int)Math.Clamp(Math.Round(session.ContentCssW * sx), 1, pixW); - cropH = (int)Math.Clamp(Math.Round(session.ContentCssH * sy), 1, pixH); + cropX = (int)Math.Clamp(Math.Round(session.ContentCssX * sx), 0, pixW - 1); + cropY = (int)Math.Clamp(Math.Round(session.ContentCssY * sy), 0, pixH - 1); + cropW = (int)Math.Clamp(Math.Round(session.ContentCssW * sx), 1, pixW - cropX); + cropH = (int)Math.Clamp(Math.Round(session.ContentCssH * sy), 1, pixH - cropY); } var outPixels = pixels; - if (cropW != pixW || cropH != pixH) + if (cropW != pixW || cropH != pixH || cropX != 0 || cropY != 0) { var dstStride = cropW * 4; outPixels = new byte[dstStride * cropH]; for (var r = 0; r < cropH; r++) - Buffer.BlockCopy(pixels, r * stride, outPixels, r * dstStride, dstStride); + Buffer.BlockCopy(pixels, (cropY + r) * stride + cropX * 4, outPixels, r * dstStride, dstStride); } var wb = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null); diff --git a/TASKS.md b/TASKS.md index 7619a7a..fc9f14e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -917,16 +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, canvas-size viewport + real-content-bounds crop):** +**Rendering model (2026-08-28, canvas-size viewport + wrapper-safe content-rect crop):** the page renders at the MASTER CANVAS size (1920×1080), stable, never tracked (no reflow/truncation; -scrollbars suppressed via `overflow:hidden`). After each navigation the manager measures the widget's -ACTUAL visible content rect — union of every visible `body *` `getBoundingClientRect` (excludes the -page's own margins/dead space, includes its top-left offset) — and **crops the capture to that rect**, -anchoring the widget at (0,0). `Stretch="Fill"` maps the frame flush under the box: the selection -box bounds the widget tight on all four sides and is pinned to (0,0) for any widget geometry. -⚠️ `ExecuteScriptAsync` JSON-encodes the returned value — the script must return a real OBJECT (not -`JSON.stringify`, which double-encodes → parse exception → crop silently skipped). Graceful-error -handling (item 9) is a follow-up. +scrollbars suppressed via `overflow:hidden`). After each navigation the manager measures the WIDGET +rect — union of every visible element's `getBoundingClientRect`, EXCLUDING elements that span +≥98% of the viewport (bodies, full-canvas background/overlay layers are layout wrappers, not +content; full-bleed widgets fall back to the full rect) — and crops the capture AT that rect's +top-left (l,t), pinning the widget to the bitmap's (0,0). `Stretch="Fill"` (+ explicit `Margin=0`, +Left/Top alignment) maps the frame flush under the box: the selection box bounds the widget tight +on all four sides, no dead space. ⚠️ `ExecuteScriptAsync` JSON-encodes the returned value — the +script must return a real OBJECT (not `JSON.stringify`, which double-encodes → parse exception → +crop silently skipped). Graceful-error handling (item 9) is a follow-up. **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).