Backgrounds: one locked Background per screen + mini rule (TASK 25)
This commit is contained in:
@@ -92,7 +92,9 @@ LayoutStore background roundtrip, LayoutStore HasBackground roundtrip + v5→v6
|
||||
ScreenCaptureManager refcount + shared-bitmap + coalescing
|
||||
(fake `IScreenCaptureSource` + a real background-STA `Dispatcher`), BackgroundTests (EnsureBackground
|
||||
insert/idempotent/heal + HasBackground gate, IsLiveCapture, DisplaySource, INPC), SceneCatalogTests
|
||||
(the five canonical scenes, Live-only background policy, EnforceBackgroundPolicy), WebcamSafeguardTests
|
||||
(the five canonical scenes; NormalizeBackgrounds heal-to-locked-model + non-canonical strip;
|
||||
EnsureBackground per-scene flavor), the TASK 25 dirty-layout heal integration test
|
||||
(RealApp + seeded temp DB: dedupe/flavor/rename/index-0 + orphaned settings purge), WebcamSafeguardTests
|
||||
(the per-scene size clamp incl. the Chat half-screen-area cap), SceneCompositorTests (the full-scene
|
||||
composite integration test: background + round webcam + mirrored/bordered images + flash; the vertical
|
||||
tier 1080×1920 crop/scale), StretchMathTests (UniformToFill cover-crop + bilinear), FfmpegLocatorTests
|
||||
@@ -180,28 +182,44 @@ The app is a **director's control surface**, not an editor. This replaces the OB
|
||||
|
||||
**Thumbnails:** `Scene.Snapshot` property. `SceneCompositor.Render()` at 320×180 → `WriteableBitmap`. Only staged scene has live backdrop render; other 4 = static snapshots.
|
||||
|
||||
### Screen backdrop capture (TASK 3 ship task #1)
|
||||
**Thumbnails:** `Scene.Snapshot` property. `RefreshSnapshotsAsync` fills minis from each scene's background art (`LoadBackgroundImage`); **minis never render live captures** — while the Live screen is staged its mini shows the green placeholder, unstaged it shows `live-background.jpg` (real-time surface = center monitor only; user decision 2026-08-23).
|
||||
|
||||
The backdrop is the **live desktop/game capture as a permanent, non-deletable bottom layer**
|
||||
rendered in every scene — the "Screen" source from the minimal set, done as content-swap
|
||||
instead of a normal draggable source.
|
||||
### Screen backdrop capture (TASK 3 ship task #1) — model reworked by TASK 25
|
||||
|
||||
- **Model (schema v6):** `Source.IsBackdrop` (persisted) marks the one backdrop per scene; `Source.CaptureKey`
|
||||
(persisted) names the target — `monitor:<n>`, `window:<hwnd>`, or `picker:<displayname>`. The backdrop is
|
||||
a real `Source` of `Type DisplayCapture`, inserted **first** (`MainViewModel.EnsureBackdrop(scene)`,
|
||||
internal static — runs on layout load + every `AddScene`, healing any scene missing one), fixed at
|
||||
X=0/Y=0/1920×1080, and excluded from drag/hit-test/remove/reorder (remove is guarded in `RemoveElement`;
|
||||
`IsDraggableElement` never matches live types; the element template sets `IsHitTestVisible=false` for
|
||||
backdrops; the row's remove button and "Remove Source" menu item are hidden). **`Scene.HasBackdrop`
|
||||
(persisted, default off) is the Live-only policy flag** — the backdrop belongs to the canonical Live
|
||||
scene alone (see `SceneCatalog`). `EnsureBackdrop` returns null for a flag-less scene, so
|
||||
Starting/BRB/Chat/Ending compose their own layers. The one-time v5→v6 backfill turns those four scenes
|
||||
off and drops their backdrop sources, and **`EnforceBackdropPolicy` (internal static, runs after every
|
||||
load)** re-normalizes the flag by scene name and strips any backdrop that lingers in a non-Live scene —
|
||||
the flag is owned by policy, never the user. There is no scene-list "Backdrop" checkbox anymore
|
||||
(the old `ToggleSceneBackdropCommand` is gone); "Change Capture…"/"Refresh Capture"/"Capture Display"
|
||||
only show in the Live scene's preview menu (`CanChangeBackdrop`). The static Background, if any,
|
||||
renders **above** the backdrop.
|
||||
**The Background is a locked per-screen layer (TASK 25): exactly one per canonical
|
||||
scene, always named "Background", pinned at index 0, never addable/removable/reorderable,
|
||||
absent from the (+) menu. Flavor is decided by scene: Live = `DisplayCapture` (the real-time
|
||||
desktop/game surface with Show Desktop / monitor switch / fullscreen-game detect, falling back
|
||||
to `live-background.jpg`); Starting/BRB/Chat/Ending = static `Background` art layers.**
|
||||
`MainViewModel.NormalizeBackgrounds(scenes)` (internal static) heals any loaded layout to this
|
||||
model on every load: keeps the correctly-flavored row (or converts a survivor in place — note
|
||||
`Source.Type`'s setter derives `IsBackground`, so conversions must re-assert the flag), drops
|
||||
duplicates/stale rows, seeds missing ones via `CreateBackground(name)`, renames, moves to index 0;
|
||||
non-canonical scenes get `HasBackground=false` and lose any backgrounds. `HealBackgrounds()`
|
||||
(instance) runs Normalize + stamps default art (`StampDefaultBackgroundAsset` reads
|
||||
`Assets/{scene}-background.jpg` pack resources through `AddAsset`; custom Browse art wins).
|
||||
This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Background`,
|
||||
`SeedLiveBackgroundAsset`, `EnsureDefaultBackground`) and `EnforceBackgroundPolicy`.
|
||||
|
||||
- **Model (schema v6):** `Source.IsBackground` (persisted) marks the one Background per scene; `Source.CaptureKey`
|
||||
(persisted) names the target — `monitor:<n>`, `window:<hwnd>`, or `picker:<displayname>`. The Live row is
|
||||
a real `Source` of `Type DisplayCapture`, inserted **first** (`MainViewModel.EnsureBackground(scene)`,
|
||||
internal static — also used by the `StagedScene` setter so a scene switch can never expose a
|
||||
missing Background), fixed at X=0/Y=0/1920×1080, and excluded from drag/hit-test/remove/reorder (remove is guarded in `RemoveElement`;
|
||||
the element template sets `IsHitTestVisible=false` for
|
||||
backgrounds; the row's remove button and "Remove Source" menu item are hidden). **`Scene.HasBackground`
|
||||
(persisted) is true for all five canonical scenes** (see `SceneCatalog.HasBackground`). Capture controls —
|
||||
"Change Capture…"/"Refresh Capture"/"Capture Display"/"Show Desktop" — exist only while the **Live** scene is
|
||||
staged (`CanChangeBackground` = staged scene is Live; the preview CanvasGrid menu binds it directly, the layer-row
|
||||
menu MultiBindings it with the row's own `IsBackground` via `Helpers/AllTrueToVisibilityConverter`). Non-Live
|
||||
screens keep the properties panel pill ("Use default background") + Browse for custom art.
|
||||
- **Settings hygiene:** `LayoutStore.Save` purges orphaned `BackgroundUseDefault_{id}` /
|
||||
`BackgroundPath_{id}` Settings keys whose id is no longer a live Source row (TASK 25 heal drops duplicates; their keys must not accumulate).
|
||||
- **Layering invariant (regression lesson, 2026-08-23):** `ActiveBackgroundImage` renders *above*
|
||||
`BackgroundImage`, so it is fed by **static-art rows only** (`Type == Background`). The capture-flavored
|
||||
Live row must never populate it or it permanently covers the desktop/game capture; Live's canvas shows
|
||||
`DisplaySource` (capture, falling back to its AssetId art). Flavor-blind `IsBackground` lookups are for
|
||||
persistence/heal/minis — not this overlay.
|
||||
- **Detection (event-driven):** `Win32FullScreenDetector` hooks `SetWinEventHook(EVENT_SYSTEM_FOREGROUND)`
|
||||
— fires on the UI thread whenever the foreground window changes. The callback runs the same
|
||||
`GetForegroundWindow` + `DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS)` + `MonitorFromWindow` +
|
||||
@@ -211,8 +229,8 @@ instead of a normal draggable source.
|
||||
`IFullScreenDetector` also exposes `GetDisplays()` (`DisplayInfo`: index/name/resolution/bounds/`IsPrimary`,
|
||||
friendly name via `EnumDisplayDevices`) + `PrimaryMonitorIndex()` for the in-app "Capture Display"
|
||||
submenu + `FullscreenMonitorChanged` event + `StartWatching()`/`StopWatching()`. At launch
|
||||
`ReacquireScreenCaptures` seeds the static fallback image (`Assets/live-backdrop.jpg`) onto the
|
||||
backdrop's `AssetId` via `SeedLiveBackdropAsset()`, then keys the backdrop to the full-screen game's
|
||||
`ReacquireScreenCaptures` heals backgrounds (`HealBackgrounds()`), then keys the Live background to the
|
||||
full-screen game's
|
||||
monitor; when **no game is detected**, `ResolveAutoCaptureKey()` returns `null` — stale `CaptureKey`
|
||||
values are cleared, no capture session is started, and `Source.DisplaySource` falls back to
|
||||
`_imageSource` (the static asset on the backdrop element). `StartWatching()` is called at the end of
|
||||
@@ -260,8 +278,8 @@ instead of a normal draggable source.
|
||||
WPF presents the window. Recorded 2026-08-13; no mitigation attempted yet (deferred by user decision).
|
||||
- **GPU posture:** same as webcam — CPU frames, WPF hardware-presents; D3DImage GPU compositing deferred
|
||||
to the encoder task.
|
||||
- **Preview watermark:** the "Preview" placeholder hides while a backdrop renders —
|
||||
`ShowPreviewPlaceholder` now also checks `BackdropImage` (raised on backdrop change), so a scene with
|
||||
- **Preview watermark:** the "Preview" placeholder hides while a background capture renders —
|
||||
`ShowPreviewPlaceholder` also checks `BackgroundImage` (raised on change), so a scene with
|
||||
live capture shows the feed instead of the "nothing here" label.
|
||||
|
||||
### Webcam capture (TASK 3 milestone 1)
|
||||
|
||||
Reference in New Issue
Block a user