Revert web render to the creator-verified-good base (70fe2b8): full-frame capture, no crop, plain Stretch=Fill

The wrapper-excluded content-union crop measured an unsettled layout on NavigationCompleted
(async fonts/images, load animations) and produced a broken/zoomed sliver — the image was gone.
Restoring the exact confirmed-good pipeline; the bounding-box dead-space problem stays open and
will not be touched without explicit approval.
This commit is contained in:
2026-08-28 11:49:21 -07:00
parent a5b9952cbd
commit 5348b5c480
4 changed files with 31 additions and 85 deletions
+13 -55
View File
@@ -29,8 +29,6 @@ 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;
@@ -161,63 +159,25 @@ public sealed class WebView2Manager : IDisposable
catch (Exception ex) { AppLog.Write($"WebView2Manager: navigate failed: {ex.Message}"); }
}
// 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
// parse would throw, silently disabling the crop).
private const string ContentBoundsScript =
"(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<els.length;i++){" +
"var st=getComputedStyle(els[i]);" +
"if(st&&st.display==='none')continue;" +
"var r=els[i].getBoundingClientRect();" +
"if(r.width||r.height)all.push(r);" +
"}}" +
"if(!all.length)return null;" +
"var content=[];" +
"for(var j=0;j<all.length;j++){" +
"var rr=all[j];" +
"if(rr.width>=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<use.length;k++){" +
"l=Math.min(l,use[k].left);" +
"t=Math.min(t,use[k].top);" +
"rt=Math.max(rt,use[k].right);" +
"bt=Math.max(bt,use[k].bottom);" +
"}" +
"l=Math.max(0,l);t=Math.max(0,t);" +
"return {l:l,t:t,w:Math.max(1,Math.min(vw-l,rt-l))," +
"h:Math.max(1,Math.min(vh-t,bt-t)),vw:vw,vh:vh};" +
"})()";
// 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.
private async Task QueryContentBoundsAsync(string sourceId)
{
if (!_sessions.TryGetValue(sourceId, out var session) || session.Disposed) return;
try
{
if (session.Control.CoreWebView2 is not { } cws) return;
var json = await cws.ExecuteScriptAsync(ContentBoundsScript);
var json = await cws.ExecuteScriptAsync(
"JSON.stringify({w:document.documentElement.scrollWidth||0," +
"h:document.documentElement.scrollHeight||0," +
"vw:window.innerWidth||0,vh:window.innerHeight||0})");
if (string.IsNullOrEmpty(json) || json == "null") return;
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
session.ViewportCssW = root.GetProperty("vw").GetDouble();
session.ViewportCssH = root.GetProperty("vh").GetDouble();
session.ContentCssX = root.GetProperty("l").GetDouble();
session.ContentCssY = root.GetProperty("t").GetDouble();
session.ContentCssW = root.GetProperty("w").GetDouble();
session.ContentCssH = root.GetProperty("h").GetDouble();
_ = CaptureFrame(sourceId);
@@ -263,25 +223,23 @@ public sealed class WebView2Manager : IDisposable
var stride = pixW * 4;
int cropX = 0, cropY = 0, cropW = pixW, cropH = pixH;
int cropW = pixW, cropH = pixH;
if (session.ViewportCssW >= 1 && session.ViewportCssH >= 1 &&
session.ContentCssW >= 1 && session.ContentCssH >= 1)
{
var sx = pixW / session.ViewportCssW;
var sy = pixH / session.ViewportCssH;
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);
cropW = (int)Math.Clamp(Math.Round(session.ContentCssW * sx), 1, pixW);
cropH = (int)Math.Clamp(Math.Round(session.ContentCssH * sy), 1, pixH);
}
var outPixels = pixels;
if (cropW != pixW || cropH != pixH || cropX != 0 || cropY != 0)
if (cropW != pixW || cropH != pixH)
{
var dstStride = cropW * 4;
outPixels = new byte[dstStride * cropH];
for (var r = 0; r < cropH; r++)
Buffer.BlockCopy(pixels, (cropY + r) * stride + cropX * 4, outPixels, r * dstStride, dstStride);
Buffer.BlockCopy(pixels, r * stride, outPixels, r * dstStride, dstStride);
}
var wb = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);