Web source: crop to the WIDGET rect (visible-element union excluding full-viewport wrappers) — kills the ghost-boundary dead space
- scrollWidth/scrollHeight returns the whole 1920x1080 canvas for widgets whose body/background spans it, so the crop kept massive transparent margins and the content floated in the Fill box. - ContentBoundsScript now unions every visible element's getBoundingClientRect, excluding elements that span >=98% of the viewport (bodies, full-canvas background layers); full-bleed widgets fall back to the full rect. Crop taken AT the rect's top-left (l,t) — content pins to bitmap (0,0). - Web Image explicitly Margin=0 + Left/Top alignment so the frame and widget coincide at the origin. - Canvas-size render untouched.
This commit is contained in:
@@ -857,6 +857,12 @@
|
|||||||
<DataTrigger Binding="{Binding IsWebSource}" Value="True">
|
<DataTrigger Binding="{Binding IsWebSource}" Value="True">
|
||||||
<Setter Property="Visibility" Value="Visible"/>
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
<Setter Property="Stretch" Value="Fill"/>
|
<Setter Property="Stretch" Value="Fill"/>
|
||||||
|
<!-- Content pins to the element's top-left (0,0): the crop
|
||||||
|
already removed leading dead space, so the frame and the
|
||||||
|
rendered widget coincide with no offset. -->
|
||||||
|
<Setter Property="Margin" Value="0"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Top"/>
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
<MultiDataTrigger>
|
<MultiDataTrigger>
|
||||||
<MultiDataTrigger.Conditions>
|
<MultiDataTrigger.Conditions>
|
||||||
|
|||||||
+15
-11
@@ -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
|
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.
|
extents regardless. Lesson: verify declared mechanics actually execute; assume nothing ran.
|
||||||
|
|
||||||
## Current iteration (web bounding box — committed, NOT pushed, verification pending)
|
## Current iteration (web bounding box — UNCOMMITTED, verification pending)
|
||||||
- Render stays the confirmed-solid 1920×1080 canvas-size viewport + `Stretch="Fill"` (UNTOUCHED).
|
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
|
Crop now uses the WIDGET rect, not scroll extents: union of every visible element's
|
||||||
`scrollWidth/scrollHeight` extents — the code that nailed right/bottom. Real-object return
|
`getBoundingClientRect`, EXCLUDING elements spanning ≥98% of the viewport (bodies,
|
||||||
keeps the crop actually executing (past cause: JSON.stringify double-encode silently disabled it).
|
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)
|
## Iteration that just failed (`b862a0f` — reverted to f39ebd9's scrollWidth/Height crop)
|
||||||
Gap moved to ALL sides. The union includes invisible full-canvas layout wrappers, so the crop rect
|
Creator still saw a massive "ghost" frame around the visible widget. Root cause: for widgets
|
||||||
came out larger than the visible widget; the offset crop (+ Rewatched fills from a subset of the
|
whose body/background spans the full 1920×1080 canvas, `scrollWidth/scrollHeight` returns the
|
||||||
canvas. Over-engineering: the fix that already worked was f39ebd9's simple scroll-extents crop.
|
WHOLE viewport → the crop keeps massive transparent margins → dead space inside the bitmap →
|
||||||
Creator's instruction: go back to that code. DONE — do not re-introduce element-union measuring.
|
content floats inside the Fill-stretched box. So the earlier "nailed right/bottom" (f39ebd9) was
|
||||||
Lesson: when a prior iteration is declared good, REVERT TO IT; do not build a cleverer replacement.
|
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.
|
||||||
+51
-16
@@ -29,6 +29,8 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
public VideoFrame? LatestFrame;
|
public VideoFrame? LatestFrame;
|
||||||
public double ViewportCssW;
|
public double ViewportCssW;
|
||||||
public double ViewportCssH;
|
public double ViewportCssH;
|
||||||
|
public double ContentCssX;
|
||||||
|
public double ContentCssY;
|
||||||
public double ContentCssW;
|
public double ContentCssW;
|
||||||
public double ContentCssH;
|
public double ContentCssH;
|
||||||
public bool Disposed;
|
public bool Disposed;
|
||||||
@@ -159,19 +161,48 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
catch (Exception ex) { AppLog.Write($"WebView2Manager: navigate failed: {ex.Message}"); }
|
catch (Exception ex) { AppLog.Write($"WebView2Manager: navigate failed: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// The page's real content rect (scrollWidth/Height) can be smaller than the
|
// The true widget rect for the crop. scrollWidth/scrollHeight is useless for
|
||||||
// viewport for widgets that don't fill it — the leftover area is dead space
|
// widgets whose body/background spans the full 1920×1080 canvas — it returns
|
||||||
// that must not stretch across the selection box. Crop the capture to the
|
// the whole viewport, so the crop keeps massive transparent margins and the
|
||||||
// content bounds (anchored at 0,0) so the box shows exactly the widget.
|
// rendered bitmap floats inside the Fill-stretched box ("ghost boundary").
|
||||||
// This restores f39ebd9's measurement (scrollWidth/Height), which the
|
// Fix: union the getBoundingClientRect of every visible element, EXCLUDING
|
||||||
// creator confirmed nailed the right and bottom of the box. The result is
|
// elements that span (nearly) the full viewport — bodies and full-canvas
|
||||||
// returned as a real JS object, NOT JSON.stringify — ExecuteScriptAsync
|
// 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-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 =
|
private const string ContentBoundsScript =
|
||||||
"({w:document.documentElement.scrollWidth||document.body.scrollWidth||0," +
|
"(function(){" +
|
||||||
"h:document.documentElement.scrollHeight||document.body.scrollHeight||0," +
|
"var vw=window.innerWidth||1,vh=window.innerHeight||1;" +
|
||||||
"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};" +
|
||||||
|
"})()";
|
||||||
|
|
||||||
private async Task QueryContentBoundsAsync(string sourceId)
|
private async Task QueryContentBoundsAsync(string sourceId)
|
||||||
{
|
{
|
||||||
@@ -185,6 +216,8 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
session.ViewportCssW = root.GetProperty("vw").GetDouble();
|
session.ViewportCssW = root.GetProperty("vw").GetDouble();
|
||||||
session.ViewportCssH = root.GetProperty("vh").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.ContentCssW = root.GetProperty("w").GetDouble();
|
||||||
session.ContentCssH = root.GetProperty("h").GetDouble();
|
session.ContentCssH = root.GetProperty("h").GetDouble();
|
||||||
_ = CaptureFrame(sourceId);
|
_ = CaptureFrame(sourceId);
|
||||||
@@ -230,23 +263,25 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
|
|
||||||
var stride = pixW * 4;
|
var stride = pixW * 4;
|
||||||
|
|
||||||
int cropW = pixW, cropH = pixH;
|
int cropX = 0, cropY = 0, cropW = pixW, cropH = pixH;
|
||||||
if (session.ViewportCssW >= 1 && session.ViewportCssH >= 1 &&
|
if (session.ViewportCssW >= 1 && session.ViewportCssH >= 1 &&
|
||||||
session.ContentCssW >= 1 && session.ContentCssH >= 1)
|
session.ContentCssW >= 1 && session.ContentCssH >= 1)
|
||||||
{
|
{
|
||||||
var sx = pixW / session.ViewportCssW;
|
var sx = pixW / session.ViewportCssW;
|
||||||
var sy = pixH / session.ViewportCssH;
|
var sy = pixH / session.ViewportCssH;
|
||||||
cropW = (int)Math.Clamp(Math.Round(session.ContentCssW * sx), 1, pixW);
|
cropX = (int)Math.Clamp(Math.Round(session.ContentCssX * sx), 0, pixW - 1);
|
||||||
cropH = (int)Math.Clamp(Math.Round(session.ContentCssH * sy), 1, pixH);
|
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;
|
var outPixels = pixels;
|
||||||
if (cropW != pixW || cropH != pixH)
|
if (cropW != pixW || cropH != pixH || cropX != 0 || cropY != 0)
|
||||||
{
|
{
|
||||||
var dstStride = cropW * 4;
|
var dstStride = cropW * 4;
|
||||||
outPixels = new byte[dstStride * cropH];
|
outPixels = new byte[dstStride * cropH];
|
||||||
for (var r = 0; r < cropH; r++)
|
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);
|
var wb = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);
|
||||||
|
|||||||
@@ -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)
|
8. ✅ Preview shows live web content (not just a blank rectangle)
|
||||||
9. ☐ Handle navigation errors, invalid URIs, timeout gracefully
|
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;
|
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
|
scrollbars suppressed via `overflow:hidden`). After each navigation the manager measures the WIDGET
|
||||||
ACTUAL visible content rect — union of every visible `body *` `getBoundingClientRect` (excludes the
|
rect — union of every visible element's `getBoundingClientRect`, EXCLUDING elements that span
|
||||||
page's own margins/dead space, includes its top-left offset) — and **crops the capture to that rect**,
|
≥98% of the viewport (bodies, full-canvas background/overlay layers are layout wrappers, not
|
||||||
anchoring the widget at (0,0). `Stretch="Fill"` maps the frame flush under the box: the selection
|
content; full-bleed widgets fall back to the full rect) — and crops the capture AT that rect's
|
||||||
box bounds the widget tight on all four sides and is pinned to (0,0) for any widget geometry.
|
top-left (l,t), pinning the widget to the bitmap's (0,0). `Stretch="Fill"` (+ explicit `Margin=0`,
|
||||||
⚠️ `ExecuteScriptAsync` JSON-encodes the returned value — the script must return a real OBJECT (not
|
Left/Top alignment) maps the frame flush under the box: the selection box bounds the widget tight
|
||||||
`JSON.stringify`, which double-encodes → parse exception → crop silently skipped). Graceful-error
|
on all four sides, no dead space. ⚠️ `ExecuteScriptAsync` JSON-encodes the returned value — the
|
||||||
handling (item 9) is a follow-up.
|
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
|
**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).
|
X→Y→W→H→URI; the ✕ button now clears the URI textbox (was reverting to the pre-accept snapshot).
|
||||||
|
|||||||
Reference in New Issue
Block a user