vv webcam Unit 1: the vertical (9:16) tier keeps its OWN camera placement per scene
The vertical tier is a centred band crop of the SAME 1920x1080 master (band x
656.25, w 607.5, h 1080), so the 16:9 camera placement (x~1408) falls outside the
cropped window and the creator is simply not in the vertical stream. Each scene
now keeps its own vertical position, size and shape; switching back to 16:9
restores the desktop layout exactly (the creator's locked table, 2026-10-17).
Geometry only - NO render-path change. The preview and the compositor both read
the element's own X/Y/W/H + ClipShape, so the tier switch swaps the LIVE geometry:
- Models/VerticalCameraLayout.cs (new): the pure math - the band, its two 607x540
areas, the six spots (top/bottom x left/centre/right), the 32px inset and the
270x152 default tile (480x270 in the 1080x1920 output) in the bottom-right spot,
the corner the 16:9 default webcam uses.
- Models/WebcamSceneConfig.cs: the vertical placement lives in seven additive
columns (VerticalX/Y/Width/Height/ClipShape/RectWidth/RectHeight; NULL = "this
scene has no vertical placement yet"). The 16:9 layout stays in the old columns
and is parked in the transient Landscape* fields while the vertical tier is
live; Begin/EndVerticalLayout + CommitLiveLayoutToVertical do the swap. The
X/Y/W/H overrides clamp every write into the band - outside it the camera is
invisible in the vertical output.
- Models/SceneElement.cs: X/Y/Width/Height became virtual so the derived element
constrains every writer (drag, resize handle, typed fields) in ONE choke point
instead of three writers each re-implementing the rule.
- ViewModels/MainViewModel.Webcam.cs + .Streaming.cs: IsVerticalTier/VerticalBand,
ResolveVerticalCameraLayout(s) (the tier swap), MoveWebcamInVerticalTier /
SnapWebcamInVerticalTier (the six-spot snap, by the tile's centre),
EnsureVerticalCameraLayout; ApplyStreamQuality is the tier-change hook. The
Live scene gets its small tile automatically on the first vertical switch (the
vertical stream would otherwise have no camera at all); the other scenes keep
their 16:9 placement until the creator drags them into the band.
- Controls/PreviewPane.xaml.cs: the drag/resize route through the VM snap under
the vertical tier instead of free-floating over the master.
Controls/LeftPanel.xaml: the webcam properties gained a Geometry section
(X/Y/W/H) for the manual entry the creator asked for, bound to the LIVE
placement, gated on the VM's IsWebcamSelected (NOT SelectedElement.IsWebcam -
the TASK 51 test finds that panel by binding text and would match twice).
- The store: seven guarded ADD COLUMNs, the INSERT/SELECT extended, and the
X/Y/W/H/ClipShape columns stay the 16:9 layout - while the vertical tier is
live the save writes the parked values there and the live ones into Vertical*.
Test: ytLive.Tests/VerticalWebcamLayoutTests.cs - the ONE Good Dog test. The
vertical tier brings the Live camera INTO the band as a small tile; dragging it to
the top-right lands it top-right inside the band; back to 16:9 the desktop layout
is restored exactly (1408/778/480x270); both placements survive a save + reload.
Full suite 429/429, 0 warnings. LayerReorderPersistenceTests.RealMouseDrag fails
ALONE on a clean tree in this environment - pre-existing flake, proven by stashing
this work, not a regression.
Derivative work: OBS has no built-in per-aspect-ratio layout storage - the
maintainers' answer is "create a second scene and add the scene you want to send
as source" (obsproject.com/forum/threads/161307) - and Streamyard's dual-aspect
guide says the same ("no built-in way to store separate layouts per aspect ratio",
so you rely on manual workarounds and plugins):
https://streamyard.com/blog/dual-aspect-ratio-streaming-tool
Our per-scene, per-tier stored placement, resolved on the tier change, is the
step past both.
Memory in the same commit: ai.md (new section), TASKS.md + TASKS/task-57-*.md
(Unit 1 shipped, Unit 2 = the Chat scene still open), HANDOFF.md, MyMistakes.md
(the per-tier geometry-swap recipe, the binding-text panel trap, the store's
webcam-identity rule) and the Models/Services/ViewModels/Controls index rows.
This commit is contained in:
@@ -1923,3 +1923,62 @@ Producer/consumer split, and **one single writer** of the bulbs:
|
||||
- Alerts **hold** until a human clears them; a sensor returning to normal notifies but does not clear.
|
||||
- Colours: leak purple, alarm red, motion orange (reserved), pump blue, doorbell green (doorbell is not
|
||||
an alert — it guards on `light_alert != 'none'` and hands back to the render).
|
||||
|
||||
## 2026-10-21 — TWO LAYOUTS FOR ONE ELEMENT ACROSS OUTPUT TIERS, WITHOUT TOUCHING THE RENDER PATH (RECIPE — TASK 57)
|
||||
|
||||
The vertical (9:16) tier is a centred **band crop** of the same 1920×1080 master, so the 16:9
|
||||
placement of an element lands outside the cropped window and the element vanishes from the
|
||||
stream. The render path (preview + compositor) reads the element's own `X`/`Y`/`Width`/`Height` +
|
||||
`ClipShape` and knows nothing about tiers — and that is the point: **do not teach the render
|
||||
path about tiers.**
|
||||
|
||||
**The recipe (four moves):**
|
||||
|
||||
1. **Park the inactive layout in the model.** The element keeps a transient copy of the layout
|
||||
that is not live (`LandscapeX/Y/Width/Height/ClipShape/RectWidth/RectHeight`) next to the
|
||||
persisted copy of the other one (`VerticalX/…`, NULL = "not set for this scene").
|
||||
2. **Swap on the tier change.** `BeginVerticalLayout(...)` parks the live layout and brings the
|
||||
other in; `EndVerticalLayout()` commits the outgoing layout back into its stored slot and
|
||||
restores the parked one. The live geometry IS the render input → the compositor and the
|
||||
preview XAML stay untouched.
|
||||
3. **Keep the persisted columns for ONE layout.** The old columns (X/Y/W/H/ClipShape) keep
|
||||
meaning "the 16:9 layout"; the save writes the parked values there while the other tier is
|
||||
live. The new columns carry the other layout. A save can then never lose either.
|
||||
4. **Commit the live copy before you persist it.** The stored copy lags the live one until the
|
||||
save — so the tier switch itself commits (otherwise switching away and back loses the
|
||||
creator's placement), and the save commits again (the DB write must not race the copy).
|
||||
|
||||
**The clamp belongs in ONE place: the model's own setters.** Make the base geometry setters
|
||||
`virtual` and override them in the derived element, so *every* writer — drag, resize handle,
|
||||
typed properties fields, code — is constrained by the same rule. Three writers each re-implementing
|
||||
"inside the band" is three chances to forget one. (This is why `SceneElement.X/Y/Width/Height`
|
||||
became virtual.)
|
||||
|
||||
**Why not the alternative:** a computed "effective geometry" property (`RenderX`) would have
|
||||
touched the compositor's element read *and* every preview binding — a render-path change for a
|
||||
geometry-only problem. The swap keeps the render path a pure consumer of the live geometry.
|
||||
|
||||
**Test seam:** the drag logic must live in a method the VM exposes, not in the control's mouse
|
||||
handler — the test then drives the placement without synthesising mouse input.
|
||||
|
||||
## 2026-10-21 — A XAML TEST THAT FINDS A PANEL BY BINDING TEXT BREAKS WHEN A SECOND PANEL SHARES THAT BINDING (TASK 57)
|
||||
|
||||
`WebcamBackgroundPillTests` locates the webcam's properties panel by **searching the XAML text
|
||||
for `SelectedElement.IsWebcam`** and then asserts on it. Adding a second panel with the same
|
||||
visibility binding made the selector match twice → `Sequence contains more than one matching
|
||||
element` → a test failure that had nothing to do with the feature under construction.
|
||||
|
||||
**Rule:** a properties panel that a test finds by binding text must have a **unique** binding.
|
||||
Use the VM's semantic flag (`IsWebcamSelected`) for a new panel instead of repeating
|
||||
`SelectedElement.IsWebcam`, and when a test selects UI by binding text, expect to have to keep
|
||||
that text unique. Cheap fix, but only if you know the test selects that way — read the test
|
||||
before you add UI.
|
||||
|
||||
## 2026-10-21 — THE LAYOUT STORE WRITES THE WEBCAM ROWS ONLY WHEN A WEBCAM IDENTITY EXISTS (TASK 57)
|
||||
|
||||
`LayoutStore.Save(scenes, webcam, …)` writes the `Webcam` row **and** the per-scene
|
||||
`WebcamSceneConfig` rows only when `webcam != null` — the config rows hang off the identity.
|
||||
A test that adds a `WebcamSceneConfig` to a scene and saves **without** an identity persists
|
||||
nothing, and the reload comes back with no camera (a confusing "my placement vanished"
|
||||
failure). Supply a `Webcam` identity in the test (`new Webcam { Id = "webcam-1", DeviceId = … }`)
|
||||
or the round-trip assertion is meaningless.
|
||||
|
||||
Reference in New Issue
Block a user