Commit Graph

153 Commits

Author SHA1 Message Date
gramps 432adfdaef perf(render): take-4 slice 2 — IsOpaque memcpy, integer bilinear, pump scratch pool
Take 4: pacing held (sync perfect) but avg render stayed 58.9ms — 2M
managed row-walk iterations + a fresh 8.3MB buffer every tick (LOH churn
into GC stalls inside the render measurement).

- VideoFrame.IsOpaque: producer-contract flag (screen capture + webcam —
  DWM/MF fill alpha 255; media/chat/web/static NOT flagged). Full-cover
  aligned opaque backdrop = ONE Buffer.BlockCopy; black pre-fill skipped
  when it covers.
- General BlitContent: integer 8.8 fixed-point bilinear + blend, row
  invariants hoisted, no per-pixel division/Math.Round. Within ±1 of the
  float reference (pixel tests allow ±2). Research per derivative-work
  rule: libyuv row/scale kernels (chromium.googlesource.com/libyuv/libyuv).
- FramePump scratch pool (max 4, length-keyed, owned-by-reference):
  release strictly AFTER SubmitFrameAsync returns (stdin write copies);
  Contains-guard makes the transition Cut alias safe.
- Removed the dead per-tick fromScene render + fromSceneProvider seam —
  BlendFrame consumes TransitionService.FromFrame captured at Start; the
  pump's render fed nothing. MainViewModel call site updated (signature).

Bugs caught by the pixel probes pre-ship (recorded MyMistakes): first
Bilinear double-shifted both stages (solid-255 sampled to ~1 -> general
path drew nothing); sentinel 0xAB collided with an x+y pixel. FakeEncoder
snapshots submitted frames (mirrors real copy semantics under recycling).

ONE integration test: Pump_Pools_ScratchBuffers_Across_Frames_Without_
Stale_Pixels (alternating backdrops + repeated backing identity). Direct
pin: Composite_OpaqueFullCover_Backdrop_CopiesEveryPixel_Into_Scratch.
Clean build 0 warnings; 59/59 per-class + RealApp boot-smoke. take 5
verdict: expect avg render <= ~10ms, ~300/300 frames. Docs same commit:
ai.md pipeline section, TASKS.md TASK 18, HANDOFF rewritten (Unit B spec
+ settled decisions queued).
2026-09-04 09:35:59 -07:00
gramps 716a77f61a fix(pump+compositor): take-3 starvation — deadline pacing + row-blit fast paths
Two defects made the producer 17x slow (37s record -> 2.1s/127-frame file,
rawvideo stamps by arrival): FramePump slept the FULL interval after each
render (period = render+submit+interval) and SceneCompositor did per-pixel
float sampling + Math.Round blends over all 2.07M master pixels, scanning the
whole destination per overlay (258ms avg render vs 1.5ms submit).

Both solutions are established, not invented — researched before coding per
the derivative-work rule:
- deadline pacing: OBS libobs/media-io/video-io.c video_thread (nextTick +=
  intervalTicks, sleep only the remainder, rebase on overrun, never burst)
- row blits: libyuv pattern (BSD-3, chromium.googlesource.com/libyuv/libyuv)
  — 1:1 aligned identity fast path, per-pixel alpha branch, integer
  fixed-point blend, overlay clipped to the intersection rect, skip the dead
  black pre-fill when the backdrop covers

ONE integration test: Pump_Paces_To_The_Deadline_Compensating_Render_Cost
(lands after a fake-seam lesson: pacing fakes must await, not complete
synchronously, or the pump loop runs inline on StartAsync and hangs vstest).
Clean build 0 warnings; FramePumpTests 10/10, SceneCompositor/SceneGraph/
SocialBar/StretchMath 20/20. Docs same commit: ai.md pipeline section,
TASKS.md TASK 18 (webcam-in-output + rename modal verified from take 3),
MyMistakes recipe, HANDOFF rewritten. Take 4 pending on the user's machine.
2026-09-03 09:02:45 -07:00
gramps 7fcb2ad3da docs: rewrite HANDOFF — tonight's 12-commit series, take-3 checklist, pending rulings, landmines refreshed (zero known failures era begins) 2026-09-01 23:27:22 -07:00
gramps 7294a1aa2d docs: HANDOFF — record the push (main == origin/main) 2026-09-01 19:35:50 -07:00
gramps b4f3b52b85 docs: rewrite HANDOFF — crash-fix outcomes, sole known failure (audio) + channel-init lead, verified test-run mechanics, 4 unpushed commits flagged 2026-09-01 19:34:10 -07:00
gramps 711ba54af7 docs: rewrite HANDOFF for session end; import the TASK 21 picker spec from the old handoff into TASKS.md
The picker-slice deliverables + loop-provider rule lived only in HANDOFF (rewrite-every-
session = lose-it). Moved into TASK 21 where it's durable; HANDOFF rewritten: locked
decisions 1-9, queue order, landmines (incl. the 2 unpushed docs commits), no-suggestions
note carried.
2026-09-01 19:04:21 -07:00
gramps 8fa54d2de7 finishing AIs work because tokens 2026-09-01 07:27:35 -07:00
gramps 4a321b19ff docs: rewrite HANDOFF for clean session end (fully pushed, project-wide read, no-suggestions note) 2026-08-31 20:30:50 -07:00
gramps 097dd0d9bd docs: hand off TASK 21 UI picker slice (acquisition + loop wiring spec)
All headless-testable TASK 21 decoder/mechanism slices shipped; the remaining UI
picker slice (AddMedia command + file dialog + Acquire/Release + Source.MediaIsLooping
-> IMediaFrameSource.Looping) is a GUI feature and is spec'd in HANDOFF for native
Windows build + verification. TASKS status + HANDOFF updated.
2026-08-31 20:17:09 -07:00
gramps 881addb5b4 TASK 21 slice 3: loop control in MediaVideoSource (process factory + Loop flag)
- IMediaFrameSource gains bool Looping.
- MediaVideoSource ctor takes Func<IDecodeProcess> processFactory instead of a
  single IDecodeProcess: a System.Diagnostics.Process can't be re-Start()ed, so
  each loop pass creates a fresh decoder. Decode wrapped in do-while(Looping):
  restart on natural EOF instead of raising Completed.
- Production wiring (MainViewModel media factory): passes the process factory
  AND FfmpegFrameRateProbe -- closes the slice-2b gap where production had no
  probe and therefore no pacing.
- Tests: loop test (single frame re-emits across passes, Completed only when
  loop cleared); fakes updated for the new interface member. Media tests 12/12,
  build 0 warnings.

Wiring Source.MediaIsLooping into the flag needs a manager-level per-path loop
provider -> lands with the UI-picker (acquisition) slice.

Derivative reference: looping media by restarting decode on EOF, standard in
playback/overlay tooling (OBS media source repeat).
2026-08-31 19:52:10 -07:00
gramps a2219be198 TASK 21 slice 2b: native-FPS pacing in MediaVideoSource (probe -> delay seam)
- MediaVideoSource takes optional IFrameRateProbe? + Func<TimeSpan,CancellationToken,Task>? delay
  seams (default Task.Delay); probes FPS once in RunAsync, delays by 1/fps after each
  emitted frame. No probe/unknown fps -> no pacing (ffmpeg pipe backpressure throttles).
- Test: MediaVideoSource_PacesFramesByProbedFps (fake probe returns 1000fps + recording
  delay; one delay per frame ~= 1ms). Media tests 5/5, build 0 warnings.

Derivative reference: per-frame delay pacing of decoded output, standard in media playback.
2026-08-31 19:47:38 -07:00
gramps 8fa78423e6 TASK 21 slice 2a: native-FPS probe seam (ffprobe parse + derive sibling)
- FfmpegFrameRateParser (pure): prefers avg_frame_rate= then r_frame_rate=,
  rational N/N/M, unknown/0 -> null.
- IFrameRateProbe + FfmpegFrameRateProbe: derives sibling ffprobe.exe from the
  located ffmpeg dir, reuses the IDecodeProcess seam for the ffprobe subprocess
  text; null if ffprobe absent.
- FfmpegLocator now also extracts ffprobe.exe (ProbeFileName) from the pinned
  archive, conditional so old caches without it degrade to no pacing.
- Tests: 6 pure parser units + 1 probe integration via fake locator/process;
  FfmpegLocatorTests still green. 15/15, 0 warnings.

Pacing (probe->delay) is slice 2b. Derivative reference: standard ffprobe
avg_frame_rate probing used across OBS/media tooling.
2026-08-31 19:45:48 -07:00
gramps 071ec8be0c TASK 21 slice 1 step 4: wire media into resolver + preview routing
Wire the MediaVideoSourceManager into the live pipeline (new partial
ViewModels/MainViewModel.Media.cs mirrors Webcam/Background partials):
readonly _mediaManager assigned in the core ctor (decode at 1920x1080 master,
dispatcher-coalesced preview), OnMediaPreviewBitmapChanged adopts the shared
bitmap onto every IsMediaSource with a matching MediaPath, a
Source { IsMediaSource, MediaPath } case in ResolveOutputFrame feeds
GetLatestFrame, and the manager is disposed on shutdown.

Source.DisplaySource now routes VideoImageSource for MediaSource (Source.cs),
so media previews/canvas show decoded video.

Compositor needs no change: BlitContent UniformToFill-scales any frame to the
element rect. Decoder/manager/seam derived-work mirrors ScreenCaptureManager
(citation in commit for slice-step-3).

Test (one unit for this change, mirroring the live-capture case):
Source_DisplaySource_IsVideoImageSource_WhenMediaSource. 14/14 media +
DisplaySource tests pass, build 0 warnings. Docs (TASKS/HANDOFF/ai.md) updated.
2026-08-31 19:32:41 -07:00
gramps 480ead0fc7 TASK 21 slice 1 step 3: IMediaFrameSource + MediaVideoSourceManager
Refactor MediaVideoSource onto IMediaFrameSource (Key/FrameAvailable/Completed/
StartAsync/StopAsync, renaming Start->StartAsync, FrameReady->FrameAvailable),
behavior preserved. Add MediaVideoSourceManager: app-wide decode-session owner
refcounted by MediaPath with Func<string,IMediaFrameSource?> factory seam,
Acquire/Release/ReleaseAll/GetLatestFrame, coalescing each file's frames onto
the UI dispatcher onto one shared WriteableBitmap; MediaFailed + PreviewBitmapChanged.

Mirrors ScreenCaptureManager (screen-capture session ownership + dispatcher
coalescing), per the codebase precedent and derivative-work rule.

Tests: MediaVideoSourceManagerTests (5 unit + 1 integration: single shared
bitmap, coalesce-to-latest); MediaVideoSourceTests updated for renames.
10/10 media tests pass, build 0 warnings. Docs (TASKS/HANDOFF/ai.md) updated.
2026-08-31 19:22:02 -07:00
gramps 8f490102ee TASK 21 Increment B: FFmpeg rawvideo video decoder -> VideoFrame
Adds the codec-agnostic decoder half of the media source. Spawns ffmpeg
with -f rawvideo -pix_fmt bgra (reusing the already-shipped ffmpeg via
IFfmpegLocator) and drains the raw BGRA stdout pipe into VideoFrames.

- Services/RawVideoFrameReader.cs: pure rawvideo BGRA stream -> frames
  (partial reads kept across Feed; no ffmpeg needed to test)
- Services/IDecodeProcess.cs + FfmpegDecodeProcess.cs: binary-stdout
  subprocess seam, mirror of the encoder's IEncoderProcess
- Services/MediaVideoSource.cs: owns the decode, raises FrameReady/Completed
- MediaVideoSourceTests: 3 pure reader + 1 integration (fake decode
  process through the real source loop, frames in order)

Reference (external scan): ffmpeg rawvideo pipe decode is the canonical
codec-agnostic frame feeds pattern (ffmpeg docs -f rawvideo; how OBS/media
pipelines push frames to a compositor). Verified: 4/4 tests, build 0 warnings.
Native-FPS pacing + resolver/compositor wiring are the next slice.
2026-08-31 18:57:29 -07:00
gramps e8ff4df05f TASK 22: global audio sync offset (positive delay at the mixer out)
Adds AudioSyncOffsetMs (0..500ms, default 0) that delays the whole
interleaved-stereo mix so audio lands on the video when it runs ahead —
OBS's documented lip-sync fix. Positive-only: advancing audio needs a
video-side delay and is out of the audio layer's scope.

- Services/Audio/AudioSyncDelay.cs: pure delay line, flushed on Configure
- AudioMixer: Func<int> syncOffsetMs seam + delay applied post-limiter
- LayoutStore.Settings + MainViewModel.Audio/VM: load/save + binding
- PreviewPane mic bar: SYNC slider + status dot (IntToSyncBrushConverter)
- AudioSyncDelayTests: identity, negative/beyond-500 clamps, 10ms→960 samples

Reference (external scan): https://obs-versions.com/blog/how-to-fix-audio-delay-on-obs
(audio ahead => positive delay). Verified: build 0 warnings; 3/3 delay tests pass.
2026-08-31 18:30:03 -07:00
gramps 670fe3a10a TASK 31: SceneGraph component + baked-crust compositor optimization
- Add ElementKind (Static/Dynamic) to SceneElement base; Source/WebcamSceneConfig classify
- Services/SceneGraph.cs: owns the Scenes collection (ViewModel's Scenes delegates to it),
  the element mutation surface (Add/Insert/Remove/Move, each invalidating the bake), and the
  queries that were scattered LINQ (GetBackground/GetWebcam/GetChatBoxes/GetSplitPoint/IsStatic)
- SceneCompositor: split-aware BakeStaticBase + CompositeLayers + Render(.., staticBase, split);
  builds/caches the static base below the split point in source-rect space
- FramePump: optional SceneGraph -> optimized bake+composite path; falls back to full render
- MainViewModel: routes element mutations through the graph; invalidates the bake on static
  layout/opacity/visibility/useDefaultBackground changes and after background heal/Ensure
- Integration test SceneGraphTests.BakedStaticBase_WithDynamicLayer_CompositesCorrectly

Derivative survey (mandated): tried before writing — OBS does per-source opacity/visibility
caching and static-scene baking; this mirrors OBS's 'cached static source' optimization.
3 documented defensive deviations from the spec (ChatOverlayLayer stays decoupled;
background helpers stay VM-static for direct testability; full facade peel deferred post-1.0)
in TASKS.md + ai.md. Scope check passed; clean build 0 warnings.
2026-08-31 17:53:34 -07:00
gramps 3bf053a3b1 TASK 31: SceneGraph design + baked-crust compositor optimization
Design captured in TASKS.md, pattern note in ai.md, handoff updated.
2026-08-31 11:38:12 -07:00
gramps 84a038c498 docs: record the true-decomposition pattern (ChatOverlayLayer) + handoff refresh
ai.md: Key patterns gains the 'true decomposition beats partial-shuffling'
rule + the glue-vs-component diagnostic (count OnPropertyChanged/SetProperty;
extract only zero/low-glue cohesive state blobs like chat's; leave binding glue
over already-extracted services alone; line count is a guideline not a goal).
ViewModels table row notes Chat.cs is now a thin facade over ChatOverlayLayer.

HANDOFF: everything through Commit G is pushed (origin/main=85893ea); the
remaining big decomposition is the scene-graph segment, flagged as design-needed
(not an unsupervised peel).
2026-08-31 11:16:02 -07:00
gramps 5b9ff5b742 handoff: Phase 3 complete (all production .cs <= 500), push pending for A-E + docs
Rewrite HANDOFF.md: Phase 2 is pushed at 1f4624c (stale claim corrected);
Phase 3 commit rows A-E + Controls/index.md listed as local-only, push pending
on explicit approval. Records the re-structuring principle (split by concern,
500-line count is a ceiling not the goal) and the session's truncation-accident
lesson. MyMistakes.md gains the never-awk-into-the-source-you-read rule.
2026-08-31 09:11:32 -07:00
gramps 1f4624c636 docs: rewrite HANDOFF for Phase 2 completion (MainWindow.xaml → 6 controls)
Working tree clean. Phase 1 (pushed) + Phase 2 (local-only, commits 12-18 with
tags) both complete. Push checkpoint reached: next step is 'git push origin main
&& git push --tags' on explicit user approval. Landmines updated with the XAML
extraction gotchas (control xmlns, name-looked-up host stubs, window-resources
converter rule).
2026-08-30 21:16:50 -07:00
gramps 14d3ed4924 docs: rewrite HANDOFF — Phase 1 (MainViewModel split) complete
Session-end state: all 11 partials landed, core 1,890 -> 758 lines. Refactor
commits 6-11 + tags are local-only pending a milestone push; Phase 2
(MainWindow.xaml -> UserControls) queued. Grep-verified per-partial using lists
recorded so no future session re-derives them.
2026-08-30 08:52:37 -07:00
gramps fc33022006 docs: HANDOFF drill — no per-commit push 2026-08-30 07:49:50 -07:00
gramps 58e7b3f3d1 docs: refresh HANDOFF — refactor Commits 3-5 landed 2026-08-30 07:36:12 -07:00
gramps abc78cd071 MainViewModel is now a partial class — scaffold for the per-area split (Commit 0)
Zero behavior change: \`public class\` -> \`public partial class\`. Each functional area
(Scenes, Background, Webcam, Audio, Trax, Socials, Streaming, Chat, Overlays,
Account, License) will move to its own file next, one commit per partial — each
a roll-back point. Docs updated in the same commit: ViewModels/index.md (split
map), ai.md (architecture table), HANDOFF.md (state). verify.sh: build 0 warnings,
only the 2 known pre-existing test failures.
2026-08-30 07:07:35 -07:00
gramps b85092b85b commit for AI code base refactor - checkpoint commit 2026-08-29 16:08:17 -07:00
gramps afd2048a0e docs: correct task statuses — TASK 4 done, TASK 3 item 15 done, full task state update 2026-08-29 11:45:53 -07:00
gramps 04eab51ead Docs: TASK 30 + ai.md layout update + HANDOFF refresh
TASKS.md: TASK 30 added (gear cleanup + record location + preview
restructure + avatar fix). ai.md: audio description updated (mic now
in preview bottom row, not footer), resolution tiers updated (bottom
bar: gear/stats/resolution), MainWindow layout description updated.
HANDOFF.md: refreshed to clean-tree state, latest commit 455a409.
2026-08-29 11:02:11 -07:00
gramps 6031a5682d Gear cleanup + Default Location for Recordings + single Start + socials hover glow
KISS: removed Gear-menu Save/SaveAs/Open Layout (redundant with auto-save; multi-layout = OBS).
App Settings: first real setting — Default Location for Recordings (Browse + Use Downloads reset,
fallback: configured → Downloads → MyVideos). Single Start button (pills = intent, button = trigger).
Socials button moved to preview bottom row, left of TRAX; hover pulses the SocialBarElement
DropShadowEffect (sine wave BlurRadius 24→48, Opacity 0.7→1.0 at 30ms). TextBox OneWay binding
fix for get-only RecordFolderDisplay. ONE fallback test. Build 0 warnings, 247 tests (2 known).
2026-08-29 10:30:40 -07:00
gramps eba6af5b9b TASK 21 Increment A: media-source model + persistence (schema migration + round-trip test)
SourceType.MediaSource added; MediaSourceType (Video/Audio) and MediaPlaybackState
(Stopped/Playing/Paused) enums. Source gains MediaPath, MediaIsLooping, MediaVolume
(0-1 clamped), MediaPlaybackState. LayoutStore: MigrateSourceTable adds 4 guarded
ALTERs (MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState); SELECT appends
indices 26-29, INSERT adds 4 columns + params. Round-trip test
MediaSource_Config_Survives_Save_And_Reload passes. Build 0 warnings, 245/247
tests (2 known pre-existing). Gated on creator run for later integration (ffmpeg).
2026-08-29 09:43:29 -07:00
gramps 46f4696db6 TASK 18: manual rename modal for finished recordings
RenameRecordingDialog (themed, mirrors GoLiveWindow chrome): pre-fills the auto stem,
Enter/OK saves, Esc/Cancel keeps the auto name, strips stray .mp4, rejects invalid
filename chars. FinalizeRecordingAsync shows it before UniquePath rename. Naming logic
still covered by RecordingFileTests; UI shell is pure. verify.sh gate: 0 warnings,
244/246 (2 known).
2026-08-29 09:24:13 -07:00
gramps be529a46d7 Tooling: add scripts/verify.sh — one pre-commit gate (clean build 0-warnings + full tests + scope check); fix incremental-build trap that hid CS8601 (2026-08-29) 2026-08-29 09:22:36 -07:00
gramps c8374bd7ed Docs: HANDOFF + TASKS reflect TASK 18 shipped as a9eb360 (working tree clean) 2026-08-29 09:13:48 -07:00
gramps a9eb360288 TASK 18: local (record-only) recording pipeline + top-bar pills/light/Start-End button UX
Encoder dual-output: StreamEnabled/RecordEnabled/RecordPath options; per-output
ffmpeg block (stream ->flv, record ->mp4); throw unless at least one output.

RecordingFile: auto-name ty-<yyyymmdd>-<start hhmm>-0000.mp4, rename-on-stop to
ty-...-<len hhmmss>.mp4 with numeric-suffix on collision. LayoutStore persists
record folder. MainViewModel: REC/ON-AIR pills, status light, single Start<->End
button (replaces Start/Stop variants), StopStream no longer signs out, explicit
session timer. About overlay widened. Sign In label + spacing before Start.

Tests: 244 passed / 246 (2 pre-existing: AudioPipeline gains-mute, RoundClip).
Build 0 warnings. Scope check clean.

Memory-map correction (derived-solution rule, 2026-08-29): image-shrink recipe was
never recorded when first done, so it was re-derived. Added Derived-solution/recipes
rule to AGENTS.md; MyMistakes.md now a permanent recipes registry (records the
verified WPF-imaging shrink recipe); schema.md rows MyMistakes as registry; ai.md
points to the rule. README hero image replaced: 3.2MB screenshot -> 1400x794 ->
188KB docs/ytLlive-preview.jpg (JPEG q82).
2026-08-29 09:02:55 -07:00
gramps 1e633f9bd9 Spin guard: derivative-work rule (AGENTS.md) + rationale (ai.md) — proactive external scan cites in commit msg; 2nd failed fix = STOP and research externally, never a 3rd guess
Project lesson cited (the 2026-08 web-box saga, 9 commits): browser sources are fixed canvases
per OBS convention — the 'gap' was a widget CSS glow, not a bug.
2026-08-28 12:30:00 -07:00
gramps e75059a795 Web-source bounding box: task COMPLETE and verified (creator: 'bounding is perfect') — docs updated, MyMistakes reset to stub, push in this commit
Root-cause note: the apparent remaining gap was a WIDGET GLOW EFFECT (the widget's own CSS glow pushed
out its perceived borders), not a code bug — the alpha-bbox already hugged the glow halo. Verified
perfect with a second widget without the effect.
2026-08-28 12:16:43 -07:00
gramps 9b56e88800 Revert ab29ec8: container==content auto-fit made it worse — back to alpha-crop with the container at element size
The container tracks the element's own Width/Height again; the crop stays in the bitmap.
2026-08-28 12:09:13 -07:00
gramps ab29ec801d Web source: container == content — element grid + selection box sized to the cropped frame's canvas extent
- FindContentBounds already trimmed the bitmap; the grid/box still rendered at the uncropped user
  size, so Fill stretched the tight widget into a bigger frame = dead-between-content-and-border.
- ContentCanvasSize maps the crop rect to canvas units (1920/pixW per axis, DPI-correct) and the
  preview event now carries (bitmap, canvasW, canvasH).
- OnWebView2PreviewBitmapChanged sets the element Width/Height to that size; the grid and the
  SelectionOverlay (both bound to the element's Width/Height) now equal the widget exactly.
- IsPreviewDragging (set in MainWindow drag/resize) suppresses auto-fit mid-gesture.
- Tests 10/10 (alpha bounds, canvas-size mapping incl. DPI, control size); 0 warnings.
2026-08-28 12:08:04 -07:00
gramps bccdb4892c Web source: crop to the frame's ALPHA bounding box — the widget's true rendered extent, no DOM timing, can't break the render
- FindContentBounds scans the Bgra32 capture for the bounding box of non-transparent pixels
  (background is injected transparent, so alpha = widget extent). Crop to that rect + Stretch=Fill
  maps the widget flush under the selection box: full-canvas widgets fall through to the full frame
  (identical to the confirmed-good image); floating widgets get tight-fitted with no dead space.
- Removes QueryContentBoundsAsync + the JS script entirely — the DOM-union attempt (a5b9952) broke
  rendering by measuring an unsettled layout on NavigationCompleted; alpha measurement is immune.
- 3 new unit tests: tight box, no-alpha no-crop, full-alpha no-crop. 7/7 pass, 0 warnings.
2026-08-28 11:53:36 -07:00
gramps 582d1f4b0f Web source: crop to the widget's true content rect (union of visible body elements) — bounding box tight on all four sides, pinned to (0,0)
- QueryContentBoundsAsync now returns a real JS object (not JSON.stringify — ExecuteScriptAsync
  double-encodes strings, which silently killed the old crop) and measures the union of every
  visible body element's getBoundingClientRect: the widget's actual rect, top-left offset included.
- CaptureFrame crops AT that offset (x,y) instead of from (0,0), so the widget anchors at origin
  and the box is flush on right/bottom. Fill maps the frame flush under the box.
2026-08-28 11:25:06 -07:00
gramps 70fe2b80b2 Web source: render at canvas size 1920x1080 — full-bleed widgets no longer truncated to upper-left 2026-08-28 11:16:46 -07:00
gramps f39ebd96ce Web source: crop capture to actual content bounds (anchor 0,0); slider click-to-position; web URI X clears; tab order 2026-08-28 11:10:49 -07:00
gramps 23874c2dd6 Web source: stable viewport (snapshot at register) + Stretch=Fill + overflow:hidden — no reflow, no scrollbars, no dead space 2026-08-28 10:56:23 -07:00
gramps 8d793af91a Web source: geometry-agnostic viewport==box model — viewport locked to element box, Stretch=Fill, no dead space 2026-08-28 10:48:53 -07:00
gramps 9d4103c44b Web source: CSS reset per document + (0,0) anchor — kill box-vs-content padding 2026-08-28 10:41:33 -07:00
gramps e917c41a48 Web source: fix rendering — fixed 1920×1080 viewport (OBS pattern), Stretch=Uniform, startup registration 2026-08-28 10:23:59 -07:00
gramps fd6e02b687 Web source: off-screen WebView2 capture + layout persistence + transparent bg
Bundle exception (2026-08-27): this lands prior-session work plus today's
transparency fix as one commit per creator instruction.

WebView2Manager (new): off-screen WebView2 control parked at -5000/-5000 in
the window's visual tree, captures via CapturePreviewAsync every 100 ms into
a BGRA8 VideoFrame. Source Width/Height sync to the control via
PropertyChanged. JS transparent-background injection runs on every
NavigationStarting/NavigationCompleted so the page's own CSS does not paint
white over the capture.

Transparency fix: DefaultBackgroundColor = System.Drawing.Color.Transparent
set on the WPF control before EnsureCoreWebView2Async. The WPF SDK doc says
the value is forwarded to the controller on init; the IDL (ICoreWebView2-
Controller2) confirms alpha=0 makes the capture alpha-preserving. The
prior-session 'CoreWebView2.BackgroundColor' path was the wrong property
name and has been replaced.

LayoutStore: WebUri column added (idempotent ALTER TABLE) and round-tripped
on save/load (schema v11).

MainViewModel: WebView2Manager constructed on Loaded; loaded web sources
re-registered; Source.WebUri PropertyChanged forwards to manager.

MainWindow: off-screen host Canvas added to visual tree.

Tests: 3 scale-sync + WebView2_DefaultBackgroundColor_Is_Transparent +
LayoutStorePersistenceTests.WebSource_WebUri_Persists. 234/236 pass
(2 pre-existing failures unchanged).

Scale bug (DPI/physical-pixel mismatch) still open.
2026-08-27 08:52:39 -07:00
gramps f81048849a Scope Lock: pre-task declaration, git history scan, pre-commit audit script 2026-08-26 09:58:33 -07:00
gramps 3a1484b3c5 TASK 20 step 2: hotkey config UI + modifier chord persistence + conflict detection 2026-08-26 06:48:14 -07:00
gramps add024a68d HANDOFF: session state, event-button task queued, no-push rule 2026-08-24 15:37:03 -07:00