diff --git a/HANDOFF.md b/HANDOFF.md index 1742004..90a8f5a 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -8,23 +8,24 @@ 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 + content crop -**Root cause finally isolated:** hosting the page at the ELEMENT-box size clips full-bleed widgets -(designed for ~1920×1080) to their upper-left chunk inside a small viewport — "image truncated to -the upper-left corner regardless of scaling." That truncation persisted across every earlier -model because all of them rendered the page at the box (or a mix); e917c41's full 1920×1080 -render was the only one that showed full content, and its letterbox dead space was purely the -display `Uniform` + the box aspect mismatch — not the render. +**Root causes finally isolated:** (1) hosting the page at the element-box size clips full-bleed +widgets to their upper-left chunk (fixed — page now renders at the master canvas 1920×1080, +stable). (2) The content-bounds crop that was supposed to kill right/bottom dead space never ran: +the script returned `JSON.stringify(...)` (a string), and `ExecuteScriptAsync` JSON-encodes the +result, so `JsonDocument.Parse` hit a string node and `GetProperty` threw → bounds stayed unset → +no crop. ALSO the intended measure was wrong: scroll extents ≠ widget rect (they include the +page's own margins). Fixed together: the script now returns a real OBJECT (union of every visible +`body *` `getBoundingClientRect`), giving the widget's true rect INCLUDING its top-left offset, +and `CaptureFrame` crops AT that offset — so the widget anchors at (0,0) and the box is tight on +all four sides. `Stretch="Fill"` maps it flush under the box. Creator confirmed render+resize are +solid at the canvas-size change; this commit delivers the tight/0,0 bounding box. -**The model (implemented, visual verification pending):** -- `Services/WebView2Manager.cs` — page renders at the MASTER CANVAS size (1920×1080), stable, - never tracked: full-bleed widgets render FULL (no upper-left truncation); dragging the box never - reflows the page; `overflow:hidden` (html+body) + transparent-bg injection. After each nav - `QueryContentBoundsAsync` reads the page's real content rect - (`scrollWidth/scrollHeight` vs `innerWidth/innerHeight`) and `CaptureFrame` **crops to the - actual content bounds anchored at (0,0)**. Immediate capture on `NavigationCompleted`. -- `MainWindow.xaml` — web `` `Stretch="Fill"`: the cropped 1920×1080 frame maps flush under - the box (Box == widget bounds at every size, pinned (0,0), no dead space). -- Invariants: **render at canvas size; crop to content; Fill to box.** +**The CURRENT MODEL:** +- `Services/WebView2Manager.cs` — page renders at 1920×1080 (stable); `overflow:hidden` + + transparent-bg injection; `QueryContentBoundsAsync` measures real widget bounds (object return + from JS, NOT JSON.stringify); `CaptureFrame` crops at the widget's (x,y) offset → anchored 0,0. +- `MainWindow.xaml` — web `` `Stretch="Fill"`. +- Invariant: **render at canvas size; crop to the widget's true bounds; Fill to the box.** **Property panel / sliders (2026-08-28):** web URI ✕ clears the textbox; web ✓/✕ icon buttons `IsTabStop="False"` so Tab walks X→Y→W→H→URI; Slider style `IsMoveToPointEnabled="True"` diff --git a/Services/WebView2Manager.cs b/Services/WebView2Manager.cs index 7a2ec62..f8ff32f 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; @@ -163,21 +165,48 @@ public sealed class WebView2Manager : IDisposable // 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. + // The widget's ACTUAL visible content bounds — the union of every visible + // body rectangle (getBoundingClientRect). This is the real widget rect, + // excluding the page's own margins/dead space, with its top-left offset. + // A real object is returned (NOT JSON.stringify — ExecuteScriptAsync + // JSON-encodes the result, which would double-encode a string). + private const string ContentBoundsScript = + "(function(){" + + "var rs=[];var body=document.body;" + + "if(body){var br=body.getBoundingClientRect();if(br.width||br.height)rs.push(br);}" + + "var els=document.querySelectorAll('body *');" + + "for(var i=0;i= 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 e49b8bf..7619a7a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -917,16 +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 + content-bounds crop):** -the page renders at the MASTER CANVAS size (1920×1080), stable, never tracked. Two earlier bugs -are both dead: (a) box-sized viewports clip full-bleed widgets to their upper-left chunk -("truncated to the upper-left corner regardless of scaling" — the image is desktop-line space), -(b) dragging the box reflowed the page (truncation/scrollbars). Scrollbars suppressed via -`overflow:hidden` html/body injection. After each navigation the manager queries the page's real -content rect (`scrollWidth/scrollHeight` vs `innerWidth/innerHeight`) and **crops the capture to -the actual content bounds, anchored at (0,0)**. `Stretch="Fill"` maps the frame flush under the -box — selection/adornment box always bounds the widget, pinned to (0,0), for any widget geometry. -Graceful-error handling (item 9) is a follow-up. +**Rendering model (2026-08-28, canvas-size viewport + real-content-bounds 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. **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).