vv webcam Unit 2: the Chat scene in the vertical tier - rect only, tile or one whole 607x540 area
The creator's locked table (2026-10-17) gives the Chat scene its own rules in the 9:16 tier: RECT ONLY - the circle option is not offered - and a size that is either the normal small tile or ENLARGED to fill one whole 607x540 area (top or bottom). Unit 1 shipped the per-scene placement; this unit is the Chat scene's own variant of it. The rule lives on the MODEL, not on the button: - Models/SceneElement.cs: ClipShape and ToggleClipShape() are now virtual. - Models/WebcamSceneConfig.cs: VerticalShapeLocked (set per scene by the VM) OVERRIDES the ClipShape property, so no writer can slip a circle in - the toggle, the Shape button and the placement swap all go through it. The lock is cleared BEFORE the 16:9 layout comes back, so the desktop shape returns. - ViewModels/MainViewModel.Webcam.cs: CanToggleClipShape (the circle is NOT offered in the vertical Chat layout - the Shape button disappears, it is not merely greyed out), IsChatSceneCamera, ResizeWebcamInVerticalTier. - Models/VerticalCameraLayout.cs: FillsArea (the "enlarged" state, derived - no new column) and SizeFor - the Chat camera's TWO sizes, the tile or the whole area, whichever is nearer the requested width, so the resize handle has two detents instead of any size in between. - An enlarged camera FILLS the area its centre is in (top or bottom), so dragging it up/down swaps the areas. - The Chat camera is auto-tiled like the Live one when the vertical tier is picked - otherwise the vertical chat stream has no camera at all. - Controls/PreviewPane.xaml.cs: the resize routes through the VM; the drag assigns, lets the model clamp into the band, then snaps (so the enlarged camera can be dragged from the top area to the bottom one). Test: ytLive.Tests/VerticalChatCameraLayoutTests.cs - the ONE Good Dog test for the unit: the vertical tier is picked -> the Chat camera comes into the band as a small RECT tile -> the circle option is not offered and cannot be set even on the model -> dragged up and enlarged it fills the TOP area exactly -> back to 16:9 the desktop layout is restored exactly and the circle option is offered again. 430/430 tests pass, 0 warnings. Research (the creator's table is the established practice, not our invention): - Streamlabs' vertical guide: "splitting the screen in half horizontally, placing the camera feed in the top half and the main content on the bottom (or vice versa) may be the bet", and a just-chatting vertical wants the camera "to take up most of the screen" (streamlabs.com/content-hub/post/overlays-vertical-and-horizontal-streaming) - Bloopbot's vertical overlay docs describe the same per-layer model we use at scene granularity: "move or resize a layer on Vertical and it stops being Auto-placed. It keeps the place you gave it, and the horizontal layout is not touched" (bloopbot.com/docs/vertical-overlays-twitch-dual-format) - our swap-at-tier-change, stored per scene. - Landmine recorded: "about the bottom 560 px of the vertical video can be covered by Twitch's chat and player controls on phones" (Bloopbot) - the Chat camera's enlarged BOTTOM area may be half-hidden by the platform chrome; TASK 53's safe-zone work decides that. Memory in the same commit: ai.md (the Chat rules, the shape lock, the tests), TASKS.md + TASKS/task-57-vertical-camera-layout.md (TASK 57 complete), HANDOFF.md, MyMistakes.md (3 recipes: lock the model property not the UI button; two detents for a discrete size set; the edit tool cannot match an oldString with an em dash), Models/ViewModels/Controls index rows.
This commit is contained in:
@@ -246,7 +246,8 @@
|
||||
Click="MirrorButton_Click"/>
|
||||
<Button Content="{Binding SelectedElement.ShapeButtonText}"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="10,4"
|
||||
Margin="8,0,0,0" Click="ShapeButton_Click"/>
|
||||
Margin="8,0,0,0" Click="ShapeButton_Click"
|
||||
Visibility="{Binding CanToggleClipShape, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="Drag to move · drag the corner dot to resize"
|
||||
Foreground="#a0a0b0" FontSize="10" Margin="0,2,0,0"/>
|
||||
|
||||
@@ -476,9 +476,11 @@ public partial class PreviewPane : UserControl
|
||||
selected.Width = newW;
|
||||
selected.Height = newH;
|
||||
|
||||
// The spot's anchor follows the new size (TASK 57).
|
||||
// TASK 57: inside the vertical band the size snaps (the Chat
|
||||
// camera has two sizes: the small tile or one whole area) and the
|
||||
// camera ends up on its spot.
|
||||
if (vm.IsVerticalTier && selected is WebcamSceneConfig verticalCam)
|
||||
vm.SnapWebcamInVerticalTier(verticalCam);
|
||||
vm.ResizeWebcamInVerticalTier(verticalCam, newW, newH);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -10,9 +10,9 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||
| `BottomBar.xaml(.cs)` | Footer (Row 3): stream stats (bitrate/fps/dropped/duration/health), resolution dropdown (gear menu moved up to TopBar 2026-09-22) |
|
||||
| `HealthBanner.xaml(.cs)` | Row 1 status banner + the 7 value→visibility converters live in the app theme, not here |
|
||||
| `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` |
|
||||
| `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials). **TASK 57:** dragging a webcam under the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` (the six-spot snap inside the cropped band) instead of free-floating over the master, and a resize re-snaps the tile onto its spot |
|
||||
| `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials). **TASK 57:** dragging a webcam under the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` (the six-spot snap inside the cropped band) instead of free-floating over the master, and a resize re-snaps the tile onto its spot (TASK 57 Unit 2: the resize routes through `MainViewModel.ResizeWebcamInVerticalTier` — the Chat camera's two sizes, then the snap) |
|
||||
| `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight |
|
||||
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice) |
|
||||
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout |
|
||||
| `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here |
|
||||
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
|
||||
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
|
||||
|
||||
+47
-22
@@ -1,9 +1,10 @@
|
||||
# HANDOFF — 2026-10-21 (vv webcam Unit 1 shipped; the previous HA-lights session's state below)
|
||||
# HANDOFF — 2026-10-21 (vv webcam Units 1+2 shipped; the previous HA-lights session's state below)
|
||||
|
||||
## At a glance
|
||||
- **Branch** main, tracking `origin/main`. **origin/main = `6be062c` = HEAD — up to date, nothing
|
||||
waiting.** The vv Unit 1 commit was **pushed on the creator's approval** (2026-10-21, after the
|
||||
creator tested the placement). Push only at sub-milestones/milestones, at the creator's word.
|
||||
- **Branch** main, tracking `origin/main`. **origin/main = `ccc1db5` = the last pushed commit; the
|
||||
vv Unit 2 work is committed on top of it.** The vv Unit 1 commit was **pushed on the creator's
|
||||
approval** (2026-10-21, after the creator tested the placement). Push only at
|
||||
sub-milestones/milestones, at the creator's word.
|
||||
- **The push path changed:** `origin` is now
|
||||
`https://llgit.llamachile.tube/gramps/LlamaCasty.git` (HTTPS + the credential helper
|
||||
`/home/gramps/bin/gitea-credential`, which reads the password from the gitignored
|
||||
@@ -11,9 +12,12 @@
|
||||
was **renamed `gramps/ytLlive` → `gramps/LlamaCasty`**, and `llgit:2214` refuses the publickey
|
||||
of this machine's key even though it IS registered (account key id 6 "Corsair VSC"). Ask the
|
||||
Gitea REST API, don't do key archaeology — recipe in `MyMistakes.md` → 2026-10-21.
|
||||
- **This session shipped vv webcam Unit 1** (the vertical 9:16 tier keeps its OWN camera placement per
|
||||
scene; 16:9 restores the desktop layout exactly). Code + memory in ONE commit; the ONE Good Dog test
|
||||
is `ytLive.Tests/VerticalWebcamLayoutTests.cs`. **Unit 2 (vv · Chat) is next** — see the vv section.
|
||||
- **This session shipped vv webcam Units 1+2** (the vertical 9:16 tier keeps its OWN camera placement
|
||||
per scene; 16:9 restores the desktop layout exactly; the Chat scene in vv is rect-only and can be
|
||||
enlarged to fill a whole 607×540 area). Code + memory in ONE commit each; the ONE Good Dog tests
|
||||
are `ytLive.Tests/VerticalWebcamLayoutTests.cs` (Unit 1) and
|
||||
`ytLive.Tests/VerticalChatCameraLayoutTests.cs` (Unit 2). **Only the creator's hands-on check is
|
||||
left** — see the vv section.
|
||||
- **The previous session (HA lights) produced NO ytLive code** — it was spent on the creator's Home
|
||||
Assistant light alert system (off-repo, `192.168.50.145`). See `MyMistakes.md` → 2026-10-21: the plan
|
||||
was one tweak to a verified-working script; the script was rewritten twice, the alert path broke, and
|
||||
@@ -58,7 +62,7 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
|
||||
"unavailable" claim was a bad read during a reload; `alert_leak_on` now also triggers on the latched
|
||||
`sensor.pump_leak_last_water_leak_alert`.
|
||||
|
||||
## vv webcam feature — locked rules (creator, 2026-10-17) — **Unit 1 shipped 2026-10-21**
|
||||
## vv webcam feature — locked rules (creator, 2026-10-17) — **Units 1+2 shipped 2026-10-21**
|
||||
| mode | shape | size | position |
|
||||
|---|---|---|---|
|
||||
| 16:9 | as today | as today | as today |
|
||||
@@ -114,6 +118,28 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
|
||||
VM's snap instead of free-floating over the master.
|
||||
- Test seam: the drag lives in a **VM method**, so the test drives it without synthesising mouse input.
|
||||
|
||||
### What shipped in Unit 2 (the Chat scene, 2026-10-21) — do not re-hunt
|
||||
- **Rect only, and the lock is on the MODEL, not the UI.** `WebcamSceneConfig.VerticalShapeLocked`
|
||||
(set per scene by the VM) **overrides the `ClipShape` property** → a locked camera cannot become
|
||||
Round by any writer (the toggle, the Shape button, the placement swap). `SceneElement.ClipShape`
|
||||
and `SceneElement.ToggleClipShape()` are `virtual` for this. The lock is cleared **before** the
|
||||
16:9 layout comes back, so the desktop shape is restored.
|
||||
- **The circle is not OFFERED:** `MainViewModel.CanToggleClipShape` is false for a Chat camera in the
|
||||
vertical tier → the Shape button in the webcam's properties **disappears** (not just disabled).
|
||||
`MainViewModel.cs` raises it on the selection change; `MainViewModel.Streaming.cs` on the tier change.
|
||||
- **Two sizes, snapped by the nearer one:** `VerticalCameraLayout.SizeFor(band, requestedWidth)` →
|
||||
the small tile (270×152) or one whole area (607.5×540). The resize handle therefore has two
|
||||
detents instead of any size in between.
|
||||
- **An enlarged camera fills the area its centre is in** — top or bottom (`VerticalCameraLayout.AreaFor`).
|
||||
- **The Chat camera is auto-tiled** like the Live one when the vertical tier is picked (otherwise the
|
||||
vertical chat stream has no camera at all). `ResolveVerticalCameraLayout` now auto-tiles Live **and**
|
||||
Chat; Starting/BRB/Ending keep their 16:9 placement.
|
||||
- The preview's resize routes through `MainViewModel.ResizeWebcamInVerticalTier` (the Chat camera's
|
||||
discrete size + the snap); the drag routes through `MoveWebcamInVerticalTier` (assign → clamp →
|
||||
snap, so the enlarged camera can be dragged from the top area to the bottom one).
|
||||
- *Test:* `ytLive.Tests/VerticalChatCameraLayoutTests.cs` — **PASSES** (in Chat under the vertical
|
||||
tier, enlarging fills the full top area and the circle option is not offered).
|
||||
|
||||
### Units (each: ONE Good Dog integration test, own commit, memory in the same commit)
|
||||
1. ✅ **vv camera layout (Unit 1)** — shipped 2026-10-21. Stored separately per scene, resolved when the
|
||||
tier changes. Live: small tile (circle or rect) snapped to one of six spots; drag like the socials +
|
||||
@@ -121,21 +147,18 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
|
||||
*Test:* `ytLive.Tests/VerticalWebcamLayoutTests.cs` — with the vertical tier active, dragging the Live
|
||||
camera to top-right places it top-right inside the band; switching back to 16:9 restores the original
|
||||
layout exactly; both placements survive a save + reload. **PASSES.**
|
||||
2. ☐ **vv · Chat (Unit 2)** — rect enforced with the circle option unavailable, plus enlarge-to-one-area
|
||||
(607×540) snapping top or bottom.
|
||||
*Test:* in Chat under the vertical tier, enlarge sets the full top area and the circle option is not
|
||||
offered. **Not started.**
|
||||
2. ✅ **vv · Chat (Unit 2)** — shipped 2026-10-21. Rect enforced with the circle option unavailable, plus
|
||||
enlarge-to-one-area (607×540) filling the top or the bottom area by where the camera is.
|
||||
*Test:* `ytLive.Tests/VerticalChatCameraLayoutTests.cs` — in Chat under the vertical tier, enlarge sets
|
||||
the full top area and the circle option is not offered. **PASSES.**
|
||||
|
||||
## Next step
|
||||
- **vv Unit 2 (the Chat scene)** — the only queued piece of this feature. Requirements are locked in
|
||||
`TASKS/task-57-vertical-camera-layout.md`: rect ONLY in the vertical tier (no circle option offered),
|
||||
size either normal or enlarged to fill one 607×540 area, same six-spot snap; and the Chat scene's
|
||||
16:9 exemption from the half-frame cap (`MaxWebcamWidthFor/HeightFor`) must not leak into the
|
||||
vertical tier.
|
||||
- **Creator check (needs the creator's hands):** switch the resolution dropdown to "Vertical 1080p60",
|
||||
confirm the Live camera tile lands inside the band and the Shape button toggles circle/rect there,
|
||||
then switch back and confirm the desktop layout is untouched. The unit test covers the geometry, not
|
||||
the eyeball.
|
||||
- **Creator check (needs the creator's hands)** — the last piece of the vv feature: switch the
|
||||
resolution dropdown to "Vertical 1080p60", confirm the Live camera tile lands inside the band and
|
||||
the Shape button toggles circle/rect there, open the **Chat** scene and confirm the Shape button is
|
||||
**gone** (rect only) and that dragging the tile up and enlarging it fills a whole 607×540 area, then
|
||||
switch back and confirm the desktop layout is untouched. The unit tests cover the geometry, not the
|
||||
eyeball.
|
||||
- Nothing else is queued for llamacasty — the creator queues work.
|
||||
|
||||
## Open (operational)
|
||||
@@ -163,7 +186,9 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
|
||||
- TASK 55 disk hygiene (`startup.log` uncapped — 23MB and growing every 5s).
|
||||
|
||||
## Tests
|
||||
- Last full run: **429/429 passed, 0 warnings** (2026-10-21, this session — the vv Unit 1 gate run).
|
||||
- Last full run: **429/430 passed, 0 warnings** (2026-10-21, this session — the vv Unit 2 gate run).
|
||||
The one failure was `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
|
||||
which **passes in isolation** (re-run: 3/3) — the known real-mouse flake, not a regression.
|
||||
- Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`,
|
||||
`MasterLimiter_CapsTheLiveMix_OnThePipe`, `LayerReorderPersistenceTests.RealMouseDrag…`,
|
||||
`Round_Clip_Corner…`.
|
||||
|
||||
@@ -111,7 +111,10 @@ public abstract class SceneElement : INotifyPropertyChanged
|
||||
public bool IsEditing { get => _isEditing; set => Set(ref _isEditing, value); }
|
||||
|
||||
private ClipShape _clipShape = ClipShape.Traditional;
|
||||
public ClipShape ClipShape
|
||||
// Virtual so a derived element can CONSTRAIN the shape (the webcam is
|
||||
// rect-only in the vertical tier's Chat layout — TASK 57) instead of every
|
||||
// writer re-implementing the rule.
|
||||
public virtual ClipShape ClipShape
|
||||
{
|
||||
get => _clipShape;
|
||||
set
|
||||
@@ -130,7 +133,7 @@ public abstract class SceneElement : INotifyPropertyChanged
|
||||
public double? RectWidth { get => _rectWidth; set => Set(ref _rectWidth, value); }
|
||||
public double? RectHeight { get => _rectHeight; set => Set(ref _rectHeight, value); }
|
||||
|
||||
public void ToggleClipShape()
|
||||
public virtual void ToggleClipShape()
|
||||
{
|
||||
if (ClipShape == ClipShape.Traditional)
|
||||
{
|
||||
|
||||
@@ -38,6 +38,22 @@ public static class VerticalCameraLayout
|
||||
public static (double Width, double Height) MaxSizeFor(Rect band)
|
||||
=> (band.Width, band.Height / 2);
|
||||
|
||||
/// <summary>True when a camera is wide enough to fill one whole area of the
|
||||
/// band — the "enlarged" Chat layout (one 607×540 area).</summary>
|
||||
public static bool FillsArea(Rect band, double width) => width >= band.Width - 1;
|
||||
|
||||
/// <summary>The two sizes a camera may take in the vertical tier's Chat
|
||||
/// layout: the small tile (normal) or one whole area (enlarged). Returns
|
||||
/// the one nearer the requested width, so the resize handle snaps between
|
||||
/// them instead of taking any size in between.</summary>
|
||||
public static (double Width, double Height) SizeFor(Rect band, double requestedWidth)
|
||||
{
|
||||
var (areaWidth, areaHeight) = MaxSizeFor(band);
|
||||
return Math.Abs(requestedWidth - TileWidth) <= Math.Abs(requestedWidth - areaWidth)
|
||||
? (TileWidth, TileHeight)
|
||||
: (areaWidth, areaHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The spot a tile snaps to: the area (top/bottom) and the column
|
||||
/// (left/centre/right) its centre falls in, then the tile's top-left for
|
||||
|
||||
@@ -97,6 +97,12 @@ public class WebcamSceneConfig : SceneElement
|
||||
/// <see cref="LandscapeX"/> and friends until the tier switches back.</summary>
|
||||
public bool VerticalLayoutLive { get; private set; }
|
||||
|
||||
/// <summary>True while the vertical placement must stay RECT — the Chat
|
||||
/// scene's vertical layout (the creator's table: "vv · Chat = rect only, no
|
||||
/// circle option"). The circle is not offered there, so the shape cannot
|
||||
/// become Round by any writer.</summary>
|
||||
public bool VerticalShapeLocked { get; internal set; }
|
||||
|
||||
/// <summary>The vertical tier's band over the master frame (transient — the
|
||||
/// VM sets it when the tier changes). While the vertical placement is live
|
||||
/// the camera's geometry is clamped to it: outside the band is invisible.</summary>
|
||||
@@ -166,6 +172,26 @@ public class WebcamSceneConfig : SceneElement
|
||||
VerticalRectHeight = RectHeight;
|
||||
}
|
||||
|
||||
// The Chat scene's vertical layout is rect-only (the creator's table, the
|
||||
// circle option is not offered there). Locking the SHAPE property — not
|
||||
// just the toggle — means no writer can slip a Round camera in: the
|
||||
// toggle, the shape button and the placement swap all go through here.
|
||||
public override ClipShape ClipShape
|
||||
{
|
||||
get => base.ClipShape;
|
||||
set => base.ClipShape = VerticalShapeLocked ? ClipShape.Traditional : value;
|
||||
}
|
||||
|
||||
public override void ToggleClipShape()
|
||||
{
|
||||
if (VerticalShapeLocked)
|
||||
{
|
||||
ClipShape = ClipShape.Traditional;
|
||||
return;
|
||||
}
|
||||
base.ToggleClipShape();
|
||||
}
|
||||
|
||||
// The geometry is clamped into the band while the vertical placement is
|
||||
// live — a camera outside the cropped window is a camera the viewers never
|
||||
// see, and every writer (drag, resize handle, the properties fields) goes
|
||||
|
||||
+3
-3
@@ -8,12 +8,12 @@ Plain data types. No logic beyond what a property can carry. See
|
||||
| `Scene.cs` | A scene: `Name`, `IsHidden`, `IsChatScene`, `IsEditing`, **`HasBackdrop`** (Live-only policy flag, default off — only the canonical Live scene has one, see `SceneCatalog`), `Elements` collection (images + webcam config), `WebcamConfig` accessor |
|
||||
| `SceneCatalog.cs` | The five canonical scenes (Starting/Live/BRB/Chat/Ending) — the product, by name: `All`, `IsCanonical`, `Is(name, canonical)` (case-insensitive trim), `IsChat`, `HasBackdrop(name)` (true only for Live). Drives the empty-DB seed, the "+" missing-scenes menu, and `MainViewModel.EnforceBackdropPolicy` |
|
||||
| `Source.cs` | A source: `SourceType` enum (DisplayCapture/WindowCapture/Background/Image/TextOverlay/ChatBox/Countdown/WebSource/MediaSource/**AlertBox (TASK 43 — the celebration zone, one per layout)** — **Webcam removed**), `X`/`Y`/`Width`/`Height`, `Opacity`, `IsEnabled`, `ImageSource` (static) / `VideoImageSource` (live frames), `DisplaySource` (`IsLiveCapture` → video, else static), `ClipShape` enum (Traditional/Round), `IsMirrored` + `MirrorScale`, `MirrorButtonText`/`ShapeButtonText`, asset identity, `IsImageSource` (XAML binds this, never `Type`), **backdrop surface**: `IsBackdrop` (permanent bottom layer, never removable), `CaptureKey` (persisted `monitor:<n>`/`window:<hwnd>`/`picker:<name>`), `IsLiveCapture` (`Type` is Display/WindowCapture) |
|
||||
| `SceneElement.cs` | Base for anything placeable in a scene: shared layout/clip/mirror/border surface, `virtual IsWebcam`/`virtual IsImageSource`/`virtual IsZoomLayer` (TASK 52); `ToggleClipShape` + persisted `RectWidth`/`RectHeight` (pre-Round rect so round→rect restores after reload); **TASK 57:** the geometry setters (`X`/`Y`/`Width`/`Height`) are `virtual` so a derived element can CONSTRAIN every write (the webcam clamps itself into the vertical tier's band) instead of every writer re-implementing the rule |
|
||||
| `VerticalCameraLayout.cs` | **TASK 57 (the "vv" feature):** the vertical (9:16) tier's camera rules as pure math — `TileWidth`/`TileHeight` (270×152 master px = 480×270 in the 1080×1920 output), `Inset` (32 master px from the band's outer edge), `IsVerticalTier(QualityOption)` (portrait tier), `AreaFor(band, bottom)` (the band splits into two 607×540 areas), `MaxSizeFor(band)`, `SpotFor(band, w, h, centreX, centreY)` (the six spots — the area and column the tile's **centre** falls in, inset, never outside the band) and `DefaultSpot` (bottom-right, the corner the 16:9 default webcam uses) |
|
||||
| `SceneElement.cs` | Base for anything placeable in a scene: shared layout/clip/mirror/border surface, `virtual IsWebcam`/`virtual IsImageSource`/`virtual IsZoomLayer` (TASK 52); `ToggleClipShape` + persisted `RectWidth`/`RectHeight` (pre-Round rect so round→rect restores after reload); **TASK 57:** the geometry setters (`X`/`Y`/`Width`/`Height`) are `virtual` so a derived element can CONSTRAIN every write (the webcam clamps itself into the vertical tier's band) instead of every writer re-implementing the rule; `ClipShape` + `ToggleClipShape()` are `virtual` too, so a derived element can CONSTRAIN the shape (the Chat scene's vertical camera is rect-only) |
|
||||
| `VerticalCameraLayout.cs` | **TASK 57 (the "vv" feature):** the vertical (9:16) tier's camera rules as pure math — `TileWidth`/`TileHeight` (270×152 master px = 480×270 in the 1080×1920 output), `Inset` (32 master px from the band's outer edge), `IsVerticalTier(QualityOption)` (portrait tier), `AreaFor(band, bottom)` (the band splits into two 607×540 areas), `MaxSizeFor(band)`, `SpotFor(band, w, h, centreX, centreY)` (the six spots — the area and column the tile's **centre** falls in, inset, never outside the band) and `DefaultSpot` (bottom-right, the corner the 16:9 default webcam uses), `FillsArea(band, width)` (the camera is wide enough to fill a whole area = "enlarged") and `SizeFor(band, requestedWidth)` (the Chat camera's TWO sizes - the small tile or one whole 607×540 area — whichever is nearer the requested width, so the resize handle has two detents) |
|
||||
| `ZoomSceneElement.cs` | **Transient zoom box (TASK 52, creator-corrected 2026-10-08):** carries the box rect AND the anchored `CaptureX`/`CaptureY` (the fixed box/2 capture window set on `ActivateZoom`; moving the box never touches it). `IsZoomActive` flips `Kind` Static→Dynamic (leaves the baked base while active); never persisted (LayoutStore save skips non-Source/non-WebcamSceneConfig rows), never shown in the layers list or the element template (`IsZoomLayer` gates both). The compositor SKIPS the active element in every layer loop and draws its 2x content LAST (layer n+1) via `ZoomMagnifier`. |
|
||||
| `Webcam.cs` | Singleton webcam identity: `Id`, `DeviceId`, `Name` (one row app-wide) |
|
||||
| `BackgroundEffect.cs` | **TASK 51:** per-webcam background effect enum `None` / `Remove` / `Blur`, carried by `WebcamSceneConfig` |
|
||||
| `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) + `HasBackgroundEffect` (shows the strength slider); **TASK 57 (the "vv" feature):** the vertical (9:16) tier's OWN placement per scene — `VerticalX/Y/Width/Height/VerticalClipShape/VerticalRectWidth/VerticalRectHeight` (persisted, NULL = no vertical placement yet) + the transient `VerticalBand`/`VerticalLayoutLive`/`LandscapeX…LandscapeRectHeight` (the parked 16:9 layout) + `BeginVerticalLayout`/`EndVerticalLayout`/`CommitLiveLayoutToVertical` (the tier swap) and the `X`/`Y`/`Width`/`Height` overrides that clamp every write into the band |
|
||||
| `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) + `HasBackgroundEffect` (shows the strength slider); **TASK 57 (the "vv" feature):** the vertical (9:16) tier's OWN placement per scene — `VerticalX/Y/Width/Height/VerticalClipShape/VerticalRectWidth/VerticalRectHeight` (persisted, NULL = no vertical placement yet) + the transient `VerticalBand`/`VerticalLayoutLive`/`LandscapeX…LandscapeRectHeight` (the parked 16:9 layout) + `BeginVerticalLayout`/`EndVerticalLayout`/`CommitLiveLayoutToVertical` (the tier swap) and the `X`/`Y`/`Width`/`Height` overrides that clamp every write into the band; `VerticalShapeLocked` (the Chat scene's vertical layout is rect-only) **overrides** `ClipShape` + `ToggleClipShape()` so no writer can slip a circle in |
|
||||
| `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)` — drives the bottom-bar dropdown and the output-rect math |
|
||||
| `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum |
|
||||
| `MicStatus.cs` | Footer mic indicator: `NotConnected` / `Connected` / `Problem` — the state behind the MIC button's status dot (red = no device/capture off, green = capturing, yellow = a requested mic failed: in use/unplugged) |
|
||||
|
||||
@@ -2021,3 +2021,60 @@ the password works, use HTTPS".
|
||||
**Also:** `ssh_askpass` is not installed in this WSL — `SSH_ASKPASS`/`GIT_ASKPASS` helpers must be
|
||||
scripts you write yourself (`chmod 700`), and they should read the secret from `CREDENTIALS.md`
|
||||
rather than embed it.
|
||||
|
||||
## 2026-10-21 — A RULE THAT APPLIES TO SOME ELEMENTS ONLY: LOCK THE MODEL PROPERTY, NOT THE UI BUTTON (RECIPE — TASK 57)
|
||||
|
||||
The creator's table says the **Chat** scene's vertical camera is **rect only — the circle option is
|
||||
not offered**, while the Live scene's camera can be a circle. The obvious implementation hides the
|
||||
Shape button for that camera. That implementation is wrong: the button is only ONE writer. The
|
||||
model, the placement swap and the load path are the others, and a rule that lives only in the UI is
|
||||
a rule that will be broken by the next writer.
|
||||
|
||||
**The recipe — lock the property, and let the UI follow:**
|
||||
1. Make the property **`virtual`** in the base class (`SceneElement.ClipShape`) and **override** it
|
||||
in the derived class (`WebcamSceneConfig`): the setter coerces the value when the lock is on.
|
||||
Make the *action* virtual too (`ToggleClipShape()`) so the toggle can no-op **without** touching
|
||||
the round-trip fields (`RectWidth/RectHeight`) — a half-applied toggle is worse than none.
|
||||
2. The lock flag lives on the model (`VerticalShapeLocked`), is set **per scene** by the view model
|
||||
(`ResolveVerticalCameraLayout`) and is **cleared BEFORE the other layout is restored** — clear it
|
||||
after and the restore is silently coerced (the desktop circle comes back as a rectangle).
|
||||
3. The UI gate is then a *read-out* of the model rule, not the rule itself: a view-model property
|
||||
(`CanToggleClipShape`) that the button binds its `Visibility` to. "Not offered" = the button is
|
||||
**gone**, not merely greyed out.
|
||||
4. Test the model path as well as the UI path: call the toggle directly and assert the shape did not
|
||||
change — the UI's absence is not the rule.
|
||||
|
||||
**The general shape:** a per-element variant of a layout rule → one `virtual` property override +
|
||||
one lock flag, with the UI's visibility derived from it.
|
||||
|
||||
## 2026-10-21 — "SIZE A OR SIZE B": GIVE THE RESIZE HANDLE TWO DETENTS, NOT A FREE SIZE (RECIPE — TASK 57)
|
||||
|
||||
The creator's table says the Chat camera in the vertical tier is either the small tile (270×152) or
|
||||
**enlarged to fill one whole 607×540 area**. The obvious implementation lets the resize handle take
|
||||
any width in between — and then the camera is neither of the two things the creator asked for, and
|
||||
the "enlarged" state has to be *guessed* from the width.
|
||||
|
||||
**The recipe:**
|
||||
- Put the allowed sizes in one pure function next to the other band math
|
||||
(`VerticalCameraLayout.SizeFor(band, requestedWidth)`) and return **the size nearest the
|
||||
requested one** — the handle then has exactly two detents and no in-between.
|
||||
- The "enlarged" state is **derived**, not stored: `FillsArea(band, width)` (the camera is wide
|
||||
enough to fill an area). No new DB column, no state to keep in sync.
|
||||
- The position of an enlarged camera is **not free**: it fills the area its **centre** is in (top or
|
||||
bottom) — `AreaFor(band, isBottom)`. Drag it up/down and it swaps areas; that IS the interaction.
|
||||
- Watch the order of the writes: set the **size first**, then the position, because a position clamp
|
||||
that knows the size (the band's edge minus the width) will otherwise fight the new size.
|
||||
|
||||
## 2026-10-21 — THE EDIT TOOL CANNOT MATCH AN oldString CONTAINING AN EM DASH (RECIPE)
|
||||
|
||||
Three `edit` calls in a row failed with "Could not find oldString" on text I had just read in the
|
||||
file. The common factor: the matched block contained the **em dash** `—` (and in one case the
|
||||
`×` multiplication sign) — the same non-ASCII typography the repo's comments are full of. The tool
|
||||
documents a retry that ignores "typographic quote, dash, and space variants", but that retry did not
|
||||
save the match.
|
||||
|
||||
**The workaround that works:** keep the `oldString` **ASCII-only**. Match the smallest unique slice
|
||||
that does not contain a dash (e.g. the `if (...) return false;` line instead of the comment above
|
||||
it), and put the new prose in the `newString` where any character is fine. When a whole stale block
|
||||
must go (e.g. half-overwritten comments), a three-line `python3` script that deletes the exact line
|
||||
range is faster and safer than fighting the matcher.
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
| 54 | **Marcom round: free-forever positioning + merch storefront** | 🔶 In progress — llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) |
|
||||
| 55 | **Disk hygiene: log rotation + low-disk guard** | ☐ Queued (2026-10-09) — `startup.log` appears uncapped; also temp alert-clip cleanup + free-space check before recording | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) |
|
||||
| 56 | **Pre-publication secret scan (full git history)** | ☐ Queued (2026-10-09) — gate before the Gitea repo goes public; report commit/path only, never values | [`TASKS/task-56-secret-scan.md`](TASKS/task-56-secret-scan.md) |
|
||||
| 57 | **Vertical (9:16) tier camera placement — the "vv" feature** | 🔶 **Unit 1 shipped 2026-10-21** (Live scene: own placement per tier — small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly). **Unit 2 open** (Chat scene: rect only, normal or enlarged to one 607×540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) |
|
||||
| 57 | **Vertical (9:16) tier camera placement — the "vv" feature** | ✅ **Both units shipped 2026-10-21** (Unit 1 = Live scene: own placement per tier — small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly. Unit 2 = Chat scene: rect only — the circle is not offered — the tile or one whole 607×540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -155,14 +155,16 @@ second encoder path needing a "redirect". Record+simulcast is one ffmpeg with tw
|
||||
- **TASK 14** — Branch 2 shipped; branch 3+ in progress
|
||||
- **TASK 21** — UI picker slice remaining (Windows-only verification pending)
|
||||
- **TASK 32–36** — all queued future work (2026-09-01)
|
||||
- **🔶 TASK 57 — the "vv" vertical camera layout.** The vertical tier is a centred band crop of
|
||||
the same master, so the 16:9 camera placement falls outside the cropped window and the creator
|
||||
is not in the vertical stream at all. **Unit 1 shipped 2026-10-21:** each scene keeps its own
|
||||
vertical placement (position, size, shape); the Live scene gets a small tile in one of six spots
|
||||
(top/bottom × left/centre/right), circle or rect, placed by drag-snap or typed into the new
|
||||
Geometry fields; switching back to 16:9 restores the desktop layout exactly. **Unit 2 is next:**
|
||||
the Chat scene in the vertical tier — rect only, either normal or enlarged to one 607×540 area
|
||||
(the creator's table, 2026-10-17). See [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md)
|
||||
- **✅ TASK 57 — the "vv" vertical camera layout** (both units shipped 2026-10-21). The vertical
|
||||
tier is a centred band crop of the same master, so the 16:9 camera placement falls outside the
|
||||
cropped window and the creator is not in the vertical stream at all. **Unit 1:** each scene keeps
|
||||
its own vertical placement (position, size, shape); the Live scene gets a small tile in one of
|
||||
six spots (top/bottom × left/centre/right), circle or rect, placed by drag-snap or typed into the
|
||||
new Geometry fields; switching back to 16:9 restores the desktop layout exactly. **Unit 2:** the
|
||||
Chat scene in the vertical tier — rect only (the circle option is not offered and the model
|
||||
refuses it), the small tile or enlarged to fill one whole 607×540 area, top or bottom by where the
|
||||
camera is (the creator's table, 2026-10-17). **Left:** the creator's hands-on check of the whole
|
||||
vv feature. See [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md)
|
||||
and `ai.md` → "Vertical (9:16) tier camera placement".
|
||||
- **Webcam resource lifecycle (2026-09-17)** — startup slice shipped (OS poll at start, single-camera
|
||||
auto-lock, persistent red alert in the Layers panel on lock failure, re-polls until resolved); gate
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here.
|
||||
|
||||
**Status:** 🔶 Unit 1 shipped 2026-10-21 (the Live scene). **Unit 2 open** (the Chat scene).
|
||||
**Status:** ✅ Both units shipped 2026-10-21 (Unit 1 = the Live scene, Unit 2 = the Chat scene).
|
||||
Only the creator's hands-on check (slice 3) is outstanding.
|
||||
|
||||
## Why
|
||||
The vertical tier (1080×1920) is a centred **band crop** of the same 1920×1080 master every
|
||||
@@ -16,7 +17,7 @@ not in the picture. Switching tiers must not cost the creator their camera.
|
||||
|---|---|---|---|
|
||||
| 16:9 | as today | as today | as today |
|
||||
| **vv · Live** | circle **or** rect, the creator's choice | small tile | one of **six** spots: top/bottom × left/centre/right |
|
||||
| vv · Chat | rect only | normal, or enlarged to one 607×540 area | the same six spots (Unit 2) |
|
||||
| vv · Chat | rect only | normal, or enlarged to one 607×540 area | the same six spots; enlarged fills the top or the bottom area |
|
||||
| Starting / BRB / Ending | unchanged | unchanged | unchanged |
|
||||
|
||||
Plus: each scene keeps its **own** vertical position, size and shape; switching back to 16:9
|
||||
@@ -47,23 +48,53 @@ restores the desktop layout **exactly**; placement is set by drag with snap (lik
|
||||
- **Manual entry.** The webcam's Layers properties gained a Geometry section (X/Y/W/H) bound to
|
||||
the **live** placement — the one the current tier shows.
|
||||
|
||||
## Design (Unit 2 — the Chat scene)
|
||||
|
||||
- **Rect only, and the lock sits on the model.** `WebcamSceneConfig.VerticalShapeLocked` (set per
|
||||
scene by the VM) **overrides the `ClipShape` property**, so a locked camera cannot become Round
|
||||
by ANY writer — the toggle, the Shape button, the placement swap. The lock is cleared *before*
|
||||
the 16:9 layout comes back, so the desktop shape is restored.
|
||||
- **The circle is not offered.** `MainViewModel.CanToggleClipShape` is false for a Chat camera in
|
||||
the vertical tier → the Shape button in the webcam's properties disappears (not just disabled).
|
||||
- **Two sizes, snapped by the nearer one.** `VerticalCameraLayout.SizeFor(band, requestedWidth)`
|
||||
returns the small tile (270×152) or one whole area (607.5×540) — whichever is nearer the
|
||||
requested width, so the resize handle has two detents instead of any size in between.
|
||||
- **An enlarged camera fills the area its centre is in** — the top one or the bottom one
|
||||
(`VerticalCameraLayout.AreaFor`).
|
||||
- **The Chat camera is auto-tiled** like the Live one when the vertical tier is picked — otherwise
|
||||
the vertical chat stream has no camera at all.
|
||||
|
||||
## Slices (one integration test each)
|
||||
|
||||
1. ✅ **Unit 1 — the Live scene** (shipped 2026-10-21): the vertical tier brings the Live camera
|
||||
into the band as a small tile; dragging it to the top-right snaps it top-right inside the
|
||||
band; switching back to 16:9 restores the desktop layout exactly; both placements survive a
|
||||
save + reload. `ytLive.Tests/VerticalWebcamLayoutTests.cs` — the ONE test for the unit.
|
||||
2. ☐ **Unit 2 — the Chat scene**: rect only (no circle) in the vertical tier, either the normal
|
||||
size or enlarged to fill one 607×540 area; the same six-spot snap. The Chat scene's 16:9
|
||||
exemption from the half-frame cap must not leak into the vertical tier.
|
||||
2. ✅ **Unit 2 — the Chat scene** (shipped 2026-10-21): in the vertical tier the Chat camera comes
|
||||
into the band as a small RECT tile; the circle option is not offered and cannot be set even on
|
||||
the model; enlarged it fills one whole 607×540 area — the top one or the bottom one, by where
|
||||
the camera is; switching back to 16:9 restores the desktop layout exactly and the circle option
|
||||
is offered again. `ytLive.Tests/VerticalChatCameraLayoutTests.cs` — the ONE test for the unit.
|
||||
3. ☐ **Creator check** (needs the creator's hands): switch the dropdown to "Vertical 1080p60",
|
||||
confirm the camera tile lands inside the band and the Shape button toggles circle/rect in the
|
||||
vertical tier, then switch back and confirm the desktop layout is untouched.
|
||||
vertical tier (and is gone in the Chat scene), enlarge the Chat camera to see it fill a whole
|
||||
area, then switch back and confirm the desktop layout is untouched.
|
||||
|
||||
## Notes
|
||||
|
||||
- OBS cannot do this (no per-aspect layout storage — the maintainers' answer is a second scene
|
||||
fed as a source); Streamyard documents the same gap. Our per-scene, per-tier stored placement
|
||||
resolved on the tier change is the step past both.
|
||||
- **Research (Unit 2, 2026-10-21).** The "one 607×540 area" rule is the established vertical-stream
|
||||
layout: Streamlabs' vertical guide says "splitting the screen in half horizontally, placing the
|
||||
camera feed in the top half and the main content on the bottom (or vice versa) may be the bet",
|
||||
and that a just-chatting vertical wants the camera "to take up most of the screen"
|
||||
(streamlabs.com/content-hub/post/overlays-vertical-and-horizontal-streaming). Bloopbot's vertical
|
||||
overlay docs describe the same per-layer model we use at scene granularity: "move or resize a
|
||||
layer on Vertical and it stops being Auto-placed. It keeps the place you gave it, and the
|
||||
horizontal layout is not touched" (bloopbot.com/docs/vertical-overlays-twitch-dual-format).
|
||||
- ⚠️ **Landmine for the bottom area:** "about the bottom 560 px of the vertical video can be covered
|
||||
by Twitch's chat and player controls on phones" (Bloopbot, above) — the Chat camera's enlarged
|
||||
**bottom** area may be half-hidden by the platform chrome. TASK 53's safe-zone work decides that.
|
||||
- The vertical tier's output crop is TASK 53's business (safe zones, caption clearance); the
|
||||
camera tile is inside the band, but the band is not yet proven against TikTok/Shorts chrome.
|
||||
|
||||
@@ -428,6 +428,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
// in, 16:9 puts the desktop layout back exactly where it was.
|
||||
OnPropertyChanged(nameof(IsVerticalTier));
|
||||
OnPropertyChanged(nameof(VerticalBand));
|
||||
OnPropertyChanged(nameof(CanToggleClipShape));
|
||||
if (ResolveVerticalCameraLayouts(VerticalCameraLayout.IsVerticalTier(quality)))
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
@@ -486,6 +486,11 @@ public partial class MainViewModel
|
||||
|
||||
private bool ResolveVerticalCameraLayout(Scene scene, WebcamSceneConfig config, bool vertical)
|
||||
{
|
||||
// The Chat scene's vertical layout is RECT-only (the creator's table:
|
||||
// "vv · Chat = rect only — no circle option"). The lock is cleared
|
||||
// BEFORE the 16:9 layout comes back, so the desktop shape is restored.
|
||||
config.VerticalShapeLocked = vertical && SceneCatalog.IsChat(scene.Name);
|
||||
|
||||
if (!vertical)
|
||||
{
|
||||
config.VerticalBand = Rect.Empty;
|
||||
@@ -509,12 +514,14 @@ public partial class MainViewModel
|
||||
return true;
|
||||
}
|
||||
|
||||
// The Live scene's camera must be INSIDE the band or the vertical
|
||||
// output has no camera at all — it starts as a small tile in the
|
||||
// bottom-right spot (the corner the 16:9 default uses). The other
|
||||
// scenes keep their 16:9 placement until the creator drags them into
|
||||
// the band (creator ruling 2026-10-17: Starting/BRB/Ending unchanged).
|
||||
if (!SceneCatalog.Is(scene.Name, SceneCatalog.Live)) return false;
|
||||
// The Live and Chat scenes are the camera scenes in the vertical tier:
|
||||
// without a placement INSIDE the band the vertical output has no camera
|
||||
// at all. They start as a small tile in the bottom-right spot (the
|
||||
// corner the 16:9 default webcam uses). Starting/BRB/Ending keep their
|
||||
// 16:9 placement (creator ruling 2026-10-17: unchanged) until the
|
||||
// creator drags them into the band.
|
||||
if (!SceneCatalog.Is(scene.Name, SceneCatalog.Live) && !SceneCatalog.IsChat(scene.Name))
|
||||
return false;
|
||||
|
||||
var spot = VerticalCameraLayout.DefaultSpot(
|
||||
VerticalBand, VerticalCameraLayout.TileWidth, VerticalCameraLayout.TileHeight);
|
||||
@@ -534,11 +541,9 @@ public partial class MainViewModel
|
||||
{
|
||||
if (!IsVerticalTier) return;
|
||||
EnsureVerticalCameraLayout(config);
|
||||
var spot = VerticalCameraLayout.SpotFor(
|
||||
VerticalBand, config.Width, config.Height,
|
||||
x + config.Width / 2, y + config.Height / 2);
|
||||
config.X = spot.X;
|
||||
config.Y = spot.Y;
|
||||
config.X = x;
|
||||
config.Y = y;
|
||||
SnapWebcamInVerticalTier(config);
|
||||
}
|
||||
|
||||
/// <summary>Snaps a camera back onto its spot — the resize handle grew or
|
||||
@@ -547,13 +552,63 @@ public partial class MainViewModel
|
||||
{
|
||||
if (!IsVerticalTier) return;
|
||||
EnsureVerticalCameraLayout(config);
|
||||
|
||||
var centreX = config.X + config.Width / 2;
|
||||
var centreY = config.Y + config.Height / 2;
|
||||
|
||||
// The enlarged Chat camera fills one whole area — the top one or the
|
||||
// bottom one, by where its centre is.
|
||||
if (VerticalCameraLayout.FillsArea(VerticalBand, config.Width))
|
||||
{
|
||||
var area = VerticalCameraLayout.AreaFor(
|
||||
VerticalBand, centreY >= VerticalBand.Top + VerticalBand.Height / 2);
|
||||
config.Width = area.Width;
|
||||
config.Height = area.Height;
|
||||
config.X = area.Left;
|
||||
config.Y = area.Top;
|
||||
return;
|
||||
}
|
||||
|
||||
var spot = VerticalCameraLayout.SpotFor(
|
||||
VerticalBand, config.Width, config.Height,
|
||||
config.X + config.Width / 2, config.Y + config.Height / 2);
|
||||
VerticalBand, config.Width, config.Height, centreX, centreY);
|
||||
config.X = spot.X;
|
||||
config.Y = spot.Y;
|
||||
}
|
||||
|
||||
/// <summary>True when this camera belongs to the Chat scene — the vertical
|
||||
/// tier's Chat layout has its own rules (rect only, two sizes).</summary>
|
||||
public bool IsChatSceneCamera(WebcamSceneConfig config)
|
||||
=> Scenes.Any(s => SceneCatalog.IsChat(s.Name) && s.Elements.Contains(config));
|
||||
|
||||
/// <summary>False when the circle option is not offered — the Chat scene's
|
||||
/// vertical layout is rect-only (the creator's table, 2026-10-17).</summary>
|
||||
public bool CanToggleClipShape => !(IsVerticalTier
|
||||
&& SelectedElement is WebcamSceneConfig selected
|
||||
&& IsChatSceneCamera(selected));
|
||||
|
||||
/// <summary>Applies a resize inside the vertical tier: the Chat camera's
|
||||
/// size is discrete — the small tile (normal) or one whole 607×540 area
|
||||
/// (enlarged) — and every camera ends up snapped to its spot.</summary>
|
||||
public void ResizeWebcamInVerticalTier(WebcamSceneConfig config, double requestedWidth, double requestedHeight)
|
||||
{
|
||||
if (!IsVerticalTier) return;
|
||||
EnsureVerticalCameraLayout(config);
|
||||
|
||||
if (IsChatSceneCamera(config))
|
||||
{
|
||||
var (w, h) = VerticalCameraLayout.SizeFor(VerticalBand, requestedWidth);
|
||||
config.Width = w;
|
||||
config.Height = h;
|
||||
}
|
||||
else
|
||||
{
|
||||
config.Width = requestedWidth;
|
||||
config.Height = requestedHeight;
|
||||
}
|
||||
|
||||
SnapWebcamInVerticalTier(config);
|
||||
}
|
||||
|
||||
/// <summary>Makes the scene's vertical placement live — creating it (a small
|
||||
/// tile in the default spot) when the scene has none yet.</summary>
|
||||
private void EnsureVerticalCameraLayout(WebcamSceneConfig config)
|
||||
|
||||
@@ -67,6 +67,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
OnPropertyChanged(nameof(IsWebcamSelected));
|
||||
OnPropertyChanged(nameof(IsElementsPanelEmpty));
|
||||
OnPropertyChanged(nameof(CanToggleClipShape));
|
||||
if (_selectedElement != null)
|
||||
_selectedElement.PropertyChanged += OnSelectedElementPropertyChanged;
|
||||
}
|
||||
|
||||
+3
-3
@@ -8,17 +8,17 @@ areas' docs in their own rows keeps this index small to read.
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `MainViewModel.cs` | Core. `public partial class MainViewModel : ViewModelBase`. **Source commands + element rows:** scenes are pure selection rows; sources carry edit/visibility/trash (`EditElementCommand` sets `element.IsEditing`, `ToggleElementVisibilityCommand` flips `IsVisible`, `RemoveSourceCommand`; Backdrop trash slot reserved via `HiddenBoolToVisibilityConverter` — TASK 13); shared `NextSourceName` (Image, Image2, Image3… no-space increment from actual names — used by `AddSource` + `AddReusedImage`), `AddImageCommand`. **Lifecycle/ctor wiring:** fields + ctor wiring (all command assignments, mixer/camera/chat/hotkey/subscription hookups), `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`, `CreateBrush` + shared `BarOnBrush`/`BarOffBrush` (referenced by Audio/Trax/Socials partials), window-geometry pass-through, `SelectedElement`/`OnSelectedElementPropertyChanged`, `ShowPreviewPlaceholder`, `BrandFlash*`, WebView2 hosting (`InitWebView2`/`RegisterLoadedWebSources`/`OnWebView2PreviewBitmapChanged`), frame-pump wiring (construction + `_rtmpUrlProvider` seam + `Dispose`, plus `ResolveOutputFrame` → `RenderChatBox`/`RenderAlertBox` for chat + alert layers) — the pump's stream-state consumers live in the Streaming partial. |
|
||||
| `MainViewModel.cs` | Core. `public partial class MainViewModel : ViewModelBase`. **Source commands + element rows:** scenes are pure selection rows; sources carry edit/visibility/trash (`EditElementCommand` sets `element.IsEditing`, `ToggleElementVisibilityCommand` flips `IsVisible`, `RemoveSourceCommand`; Backdrop trash slot reserved via `HiddenBoolToVisibilityConverter` — TASK 13); shared `NextSourceName` (Image, Image2, Image3… no-space increment from actual names — used by `AddSource` + `AddReusedImage`), `AddImageCommand`. **Lifecycle/ctor wiring:** fields + ctor wiring (all command assignments, mixer/camera/chat/hotkey/subscription hookups), `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`, `CreateBrush` + shared `BarOnBrush`/`BarOffBrush` (referenced by Audio/Trax/Socials partials), window-geometry pass-through, `SelectedElement`/`OnSelectedElementPropertyChanged` (the selection change also raises `CanToggleClipShape` — TASK 57), `ShowPreviewPlaceholder`, `BrandFlash*`, WebView2 hosting (`InitWebView2`/`RegisterLoadedWebSources`/`OnWebView2PreviewBitmapChanged`), frame-pump wiring (construction + `_rtmpUrlProvider` seam + `Dispose`, plus `ResolveOutputFrame` → `RenderChatBox`/`RenderAlertBox` for chat + alert layers) — the pump's stream-state consumers live in the Streaming partial. |
|
||||
| `MainViewModel.Account.cs` | Session + connected account: `LoadSavedSessionAsync` (DPAPI restore at startup), `SignInAsync`/`ChangeAccountAsync`/`SignOutYouTubeAsync`, `IsConnected`/`AccountAvatarUrl`/`AccountDisplayName`/`AccountStatusLightToolTip` via `SyncConnectedAccount` (cleared on End). |
|
||||
| `MainViewModel.License.cs` | Polar license (freemium): `LicenseKey`/`LicenseMessage`/`IsLicenseValidating`/`IsLicenseEntryOpen`/`IsHubVisible`/`IsPremium`, `OfflineGracePeriod`, `ValidateLicenseAtStartupAsync`/`ActivateLicenseAsync`. |
|
||||
| `MainViewModel.Scenes.cs` | Five-scene catalog: `Scenes` + `StagedScene`/`LiveScene`/`IsEditMode`, `AddScene` (canonical names only), `MissingScenes`/`ShowAddScene`/`AddSceneCommand`; scene wiring `OnScenesChanged`/`WireScene`/`UnwireScene`; transitions `SelectedTransitionType`/`TransitionDurationMs`/`TransitionToScene`/`ApplyTransitionToLive`; global hotkeys `HandleHotkey`/`OpenHotkeyConfig`/`RefreshHotkeyLabels`; scene-gating hints (`CanChangeBackground`). |
|
||||
| `MainViewModel.Zoom.cs` | **Zoom box (TASK 52, creator-corrected 2026-10-08)** `ActivateZoom`/`ToggleZoom`/`DismissZoom`/`StartZoomMove`/`UpdateZoomMove`/`EndZoomMove` + `RefreshZoomUi`/`RebuildDimRectsForZoom`. Right-clicking the live preview anchors a FIXED **capture window** (the box/2 region centred on the click) and parks a movable **2x magnifier**. The box shows a live always-on 2x view of the anchored capture; **dragging moves only the box — `CaptureX/Y` are deliberately untouched in `UpdateZoomMove`** (creator ruling). Each shape is centred on the click and clamped independently (box to the master, capture to box/2 windows): Circle = square, VerticalRectangle = 9:16, HorizontalRectangle = **4:3** (w=h·4/3 with h=0.7·1080; NOT 16:9 — first cut read as a square). The transient `ZoomSceneElement` mirrors box + capture; the output compositor draws it **LAST (layer n+1)** from the already-composed frame (`SceneCompositor.BlitZoom`→`ZoomMagnifier`) so it captures EVERY layer. No per-frame builder/cache — the resolver case that built `ZoomFrameBuilder` frames was removed. `BuildCompositorOptions` (internal = test seam) always returns the tier crop — the box is never the crop source. |
|
||||
| `MainViewModel.Background.cs` | Screen backdrop (Live-only by policy): `ScreenCaptureManager` + `ScreenCaptureSourceFactory` (WinRT GraphicsCapture); background model `EnsureBackground`/`CreateBackground`/`NormalizeBackgrounds`/`HealBackgrounds`/`StampDefaultBackgroundAsset`; screen capture `ReacquireScreenCaptures`/`ResolveAutoCaptureKey`/`RedesignateBackgroundAsync`/`ClearBackgroundCaptureAsync`/`SetBackgroundCapture`/`OnScreenPreviewBitmapChanged`/`OnFullscreenMonitorChanged`/`RefreshBackgroundAutoCapture`/`BackgroundShowDesktop` (keys on full-screen-game monitor or primary display via `Win32FullScreenDetector`); display picker `Displays`/`SetBackgroundDisplayCommand`; preview fns `ActiveBackgroundImage`/`BackgroundImage`/`UpdateActiveBackground`/`UpdateBackgroundImage`/`RefreshSnapshotsAsync`/`LoadBackgroundImage`/`CreatePlaceholderSnapshot`; `ChangeCaptureCommand`/`RefreshCaptureCommand`/`ToggleShowDesktopCommand`/`BrowseBackgroundCommand`. `ShowPreviewPlaceholder` stays in core. |
|
||||
| `MainViewModel.Webcam.cs` | Webcam identity: `_webcam`/`ReacquireWebcam`/`SwapWebcamIdentityAsync`; per-scene configs `AddWebcamToStagedSceneAsync`/`ShowWebcamInStagedScene`/`ChangeWebcamAsync` (Windows picker always opens); capture `OnCameraPreviewBitmapChanged`/`OnCameraFailed`; gating `CanAddWebcam`/`CanChangeWebcam`/`CanShowWebcamInStagedScene`/`IsWebcamSelected`/`WebcamError`; size safeguards `WebcamMax*`/`ClampWebcamToBounds`/`HealLegacySquareRect`; `AddWebcamCommand`/`ChangeWebcamCommand`/`ShowWebcamCommand`. **TASK 57 (the "vv" feature):** the vertical (9:16) tier's camera placement — `IsVerticalTier`/`VerticalBand` + `ResolveVerticalCameraLayouts`/`ResolveVerticalCameraLayout` (the tier swap: the vertical tier brings each scene's own vertical placement in, the Live scene gets its small tile on first use, 16:9 puts the desktop layout back exactly) + `MoveWebcamInVerticalTier`/`SnapWebcamInVerticalTier` (the six-spot snap) + `EnsureVerticalCameraLayout`. |
|
||||
| `MainViewModel.Webcam.cs` | Webcam identity: `_webcam`/`ReacquireWebcam`/`SwapWebcamIdentityAsync`; per-scene configs `AddWebcamToStagedSceneAsync`/`ShowWebcamInStagedScene`/`ChangeWebcamAsync` (Windows picker always opens); capture `OnCameraPreviewBitmapChanged`/`OnCameraFailed`; gating `CanAddWebcam`/`CanChangeWebcam`/`CanShowWebcamInStagedScene`/`IsWebcamSelected`/`WebcamError`; size safeguards `WebcamMax*`/`ClampWebcamToBounds`/`HealLegacySquareRect`; `AddWebcamCommand`/`ChangeWebcamCommand`/`ShowWebcamCommand`. **TASK 57 (the "vv" feature):** the vertical (9:16) tier's camera placement — `IsVerticalTier`/`VerticalBand` + `ResolveVerticalCameraLayouts`/`ResolveVerticalCameraLayout` (the tier swap: the vertical tier brings each scene's own vertical placement in, the Live scene gets its small tile on first use, 16:9 puts the desktop layout back exactly) + `MoveWebcamInVerticalTier`/`SnapWebcamInVerticalTier` (the six-spot snap) + `EnsureVerticalCameraLayout`; the Chat scene's own rules (Unit 2) — `IsChatSceneCamera`/`CanToggleClipShape` (the circle is NOT offered in the vertical Chat layout) and `ResizeWebcamInVerticalTier` (the Chat camera's two sizes, then the snap; the Live camera resizes freely inside the band) |
|
||||
| `MainViewModel.Audio.cs` | **Mic bar:** read-only realtime meter `AudioLevel`/`MeterFillWidth`/`MeterBrush` (fill = `Math.Min(1, AudioLevelMeter.ToDisplay((float)AudioLevel) * MicVolume)`, green→yellow→red at 60%/80% zones), `MicVolume` (0.8 default; drives `MicMuted` ⇔ volume 0; unmute flash `BeginVolumeFlash`/`EndVolumeFlash`/`CancelVolumeFlash`), `MicSourceName` (left-justified inside the bar, persists via `LayoutStore.Settings`), `ToggleMicMute`/`PickMicrophone` (`MicPickerDialog`; picked device swaps the live source via `_audioMixer.RestartMic()`, loopback keeps running), status dot `MicStatus`/`MicStatusBrush`/`MicStatusToolTip` (`Models.MicStatus`: green Connected / yellow Problem / red no-device), capture starts once at startup (`StartMicCaptureAsync`) — NOT go-live. **Game audio bar:** overlaid bottom-center, always visible (show/hide gating removed 2026-08-15); `GameAudioLevel` (loopback via `OnLoopbackLevelChanged`), `GameAudioVolume`/`GameMuted`/`GameMuteText`/`ToggleGameMute`/`PushSystemVolume`, mirror volume flash `SetGameVolumeAdjusting`/`BeginGameVolumeFlash`/`EndGameVolumeFlash`/`CancelGameVolumeFlash`. Mixer handlers `OnMicLevelChanged`/`OnLoopbackLevelChanged`/`OnMicConnected`/`OnMicFailed`. |
|
||||
| `MainViewModel.Trax.cs` | Background music (TASK 9, rides the loopback into the stream): `_musicPlayer` (`MusicPlayer`), `IsMusicPlaying`/`TraxError`/`TraxDotBrush`/`TraxToolTip`, interaction `TraxLeftClick`/`TraxRightClick`/`PickTraxTrack`, playback `LoadTrax`/`PlayTrax`/`PauseTrax`/`OnTraxPlaybackEnded`/`RefreshTraxUi`, `UpdateTraxLocalGain` (mirrors the game bar's mute onto `MusicPlayer.LocalGain` so the track ducks/restores with desktop audio). |
|
||||
| `MainViewModel.Socials.cs` | Social bar (global resource, overlaid on every scene): `_socials`/`_socialBarFrame`/`_socialValidator` (`ISocialValidator`/`HttpSocialValidator`), `HasSocials`/`SocialBarVisible`/`SocialBarDotBrush`/`SocialBarGlowBrush`/`Socials`/`SocialBarTop`, `OpenSocialDialogCommand`/`OpenSocialDialog` (spawns `SocialsDialogViewModel` + window; saved copy replaces `_socials`), `NotifySocialsChanged`/`RenderSocialBarFrame` (`SocialBarRenderer` → immutable `_socialBarFrame` the frame pump reads), fediverse heal `HealFediverseSoftwareAsync` (static, test-facing) + `HealFediverseSoftwareInBackground`, bar edge toggle `SetSocialBarPosition`/`ToggleSocialBarPosition`. `BarOnBrush`/`BarOffBrush`/`CreateBrush` stay in core (shared tokens). |
|
||||
| `MainViewModel.Streaming.cs` | Go-live/record + health + quality tiers + session UI. **Stream state:** `StreamStatus`/`StreamVisibility` setters + `_streamStatus` field, `IsOffline`/`IsLive`/`IsLivePrivate`. **Go-live flow:** `StartSession` (routes by pill state) → `BeginRecordOnly` (signed-out local recording) or `BeginGoLive` (GoLive dialog → captures title/description/visibility + `BroadcastForm.CaptureGoLive` → `PrepareAndStartLiveAsync`: reusable stream → `CreateBroadcast` (stores `_currentBroadcastId`) → empty-metadata warn → chat poll start (liveChatId) → `_framePump.StartAsync()` + health poll; failures → `StreamStatus.Error`, never a crash); `StartRecordFile` (auto-named `ty-…-0000.mp4`) + `FinalizeRecordingAsync` (rename modal, `UniquePath`); `StopStream` (posts Offline, stops chat/audio pipe/pump, clears `_currentBroadcastId`; session/token deliberately NOT cleared — sign-out stays explicit). **Health:** `CurrentHealth`/`ResetHealth`/`ApplyHealth`/`OnFramePumpHealthUpdated` (marshal `FramePump.HealthUpdated` off the stderr thread), `PollHealthAsync`/`OnHealthPollTick`/`ApplyHealthIssue` (report-by-exception banner `HealthIssueBanner`/`HealthIssueBackground` via `StreamHealthReporter`). **Session timer:** `_liveTimer` (ctor-wired) `UpdateSessionTimer`/`OnLiveTimerTick`/`LiveElapsedText`/`_recDotPulse`. **Start/End UI:** mode switch `RecordPillOn`/`OnAirPillOn`, top-bar worlds (`ShowIdleWorld`/`ShowRecordAction`/`ShowStreamActions`/`ShowSignInButton`/`ShowAvatarButton`, reality line `RunningVisible`/`RunningText`/`RunningDotBrush`, Test gate `ShowTestButton`/`CanStartTest` — `RaiseTopBarModes()`), `ShowPrimaryStartButton`/`CanStartSession`/`ShowEndStreamButton`/`EndButtonText`, REC sign `RecDotBrush`/`RecTextBrush`/`RecDotOpacity`/`OnAirBrush`, live window tokens `WindowTitle`/`TopBarBackground`/`PreviewGlow*` via `UpdateLiveVisuals`; `ChooseRecordFolder`/`ResetRecordFolder`/`_recordFolder`/`RecordFolderDisplay`/`DefaultRecordFolder`. **Resolution dropdown:** `QualityOptions`/`SelectedQuality`/`ResolutionHelp`/`ApplyStreamQuality` + `OutputRect*`/`DimRects`/`IsOutputCropped`/`ResolutionBadgeText` over the 1920×1080 master; **TASK 57:** the tier change also resolves every scene's camera placement (the vertical tier brings the vertical placements in, 16:9 restores the desktop layout — see `MainViewModel.Webcam.cs`). **Pipeline bits:** `_framePump`/`_liveTimer`/`_healthPollTimer` fields, `BuildEncoderOptions`. Commands `StartStreamCommand`/`EndStreamCommand`/`ChooseRecordFolderCommand`/`ResetRecordFolderCommand`. Broadcast pull-out: `CurrentBroadcastId`/`BroadcastForm`. |
|
||||
| `MainViewModel.Streaming.cs` | Go-live/record + health + quality tiers + session UI. **Stream state:** `StreamStatus`/`StreamVisibility` setters + `_streamStatus` field, `IsOffline`/`IsLive`/`IsLivePrivate`. **Go-live flow:** `StartSession` (routes by pill state) → `BeginRecordOnly` (signed-out local recording) or `BeginGoLive` (GoLive dialog → captures title/description/visibility + `BroadcastForm.CaptureGoLive` → `PrepareAndStartLiveAsync`: reusable stream → `CreateBroadcast` (stores `_currentBroadcastId`) → empty-metadata warn → chat poll start (liveChatId) → `_framePump.StartAsync()` + health poll; failures → `StreamStatus.Error`, never a crash); `StartRecordFile` (auto-named `ty-…-0000.mp4`) + `FinalizeRecordingAsync` (rename modal, `UniquePath`); `StopStream` (posts Offline, stops chat/audio pipe/pump, clears `_currentBroadcastId`; session/token deliberately NOT cleared — sign-out stays explicit). **Health:** `CurrentHealth`/`ResetHealth`/`ApplyHealth`/`OnFramePumpHealthUpdated` (marshal `FramePump.HealthUpdated` off the stderr thread), `PollHealthAsync`/`OnHealthPollTick`/`ApplyHealthIssue` (report-by-exception banner `HealthIssueBanner`/`HealthIssueBackground` via `StreamHealthReporter`). **Session timer:** `_liveTimer` (ctor-wired) `UpdateSessionTimer`/`OnLiveTimerTick`/`LiveElapsedText`/`_recDotPulse`. **Start/End UI:** mode switch `RecordPillOn`/`OnAirPillOn`, top-bar worlds (`ShowIdleWorld`/`ShowRecordAction`/`ShowStreamActions`/`ShowSignInButton`/`ShowAvatarButton`, reality line `RunningVisible`/`RunningText`/`RunningDotBrush`, Test gate `ShowTestButton`/`CanStartTest` — `RaiseTopBarModes()`), `ShowPrimaryStartButton`/`CanStartSession`/`ShowEndStreamButton`/`EndButtonText`, REC sign `RecDotBrush`/`RecTextBrush`/`RecDotOpacity`/`OnAirBrush`, live window tokens `WindowTitle`/`TopBarBackground`/`PreviewGlow*` via `UpdateLiveVisuals`; `ChooseRecordFolder`/`ResetRecordFolder`/`_recordFolder`/`RecordFolderDisplay`/`DefaultRecordFolder`. **Resolution dropdown:** `QualityOptions`/`SelectedQuality`/`ResolutionHelp`/`ApplyStreamQuality` + `OutputRect*`/`DimRects`/`IsOutputCropped`/`ResolutionBadgeText` over the 1920×1080 master; **TASK 57:** the tier change also resolves every scene's camera placement (the vertical tier brings the vertical placements in, 16:9 restores the desktop layout — see `MainViewModel.Webcam.cs`). **Pipeline bits:** `_framePump`/`_liveTimer`/`_healthPollTimer` fields, `BuildEncoderOptions`. Commands `StartStreamCommand`/`EndStreamCommand`/`ChooseRecordFolderCommand`/`ResetRecordFolderCommand`. Broadcast pull-out: `CurrentBroadcastId`/`BroadcastForm`. **TASK 57 (Unit 2):** the tier change also raises `CanToggleClipShape` (the circle option disappears in the vertical Chat layout). |
|
||||
| `MainViewModel.Overlays.cs` | Settings/bug/feature/about panels + creator-hub links: panel state `IsSettingsOpen`/`IsBugOpen`/`IsFeatureOpen`/`IsAboutOpen` (via `SetPanel` → `IsAnyOverlayOpen`), `IsLicensesOpen` (flips `IsHubVisible`), `OverlayTitle`, `ShowOverlay` (one-panel exclusivity) + `ShowLicenses` (loads shipped `THIRD-PARTY-NOTICES.txt` into `LicensesText` on first open, never the OS viewer); bug/feature report `BugReportText`/`BugReportEmail`/`FeatureRequestText`/`FeatureRequestEmail` + `SubmitBug`/`SubmitFeature`/`ComposeEmail`/`OpenUrl`; About hub consts `ChannelUrl`/`MastodonUrl`/`CoffeeUrl` (public, integration-test live) + `PremiumUrl` (tabled billing seam — TASK 12 → greyed "Unlock Premium" lights up when filled) + `IsPremiumAvailable` + `SupportEmail` + `AppVersionLabel`; commands `OpenSettingsCommand`/`OpenBugCommand`/`OpenFeatureCommand`/`OpenAboutCommand`/`CloseOverlayCommand`/`SubmitBugCommand`/`SubmitFeatureCommand`/`OpenChannelCommand`/`OpenMastodonCommand`/`OpenCoffeeCommand`/`OpenPremiumCommand`/`OpenLicensesCommand`. Ctor command assignments stay in core. |
|
||||
| `MainViewModel.Chat.cs` | Chat overlay + preview pipeline: `ChatMessages` (500-message cap; clear-on-fade), `OnChatMessageReceived` (marshal `_youtubeChat.MessageReceived` onto the dispatcher, switch previews mock→real, then feed **both** component layers), fade `_chatFadeTimer`/`ResetChatFadeTimer` (shortest `ChatFadeSeconds` across chat layers); live render `RenderChatBox` (called from `ResolveOutputFrame`, stays in core) + preview render `UpdateChatBoxPreview` (bitmap onto `chatBox.VideoImageSource`); mock preview `MockChatMessages`/`StartMockChatPreview`/`StopMockChatPreview`/`RunMockChatPreviewAsync`/`_mockChatCts`; `UpdateActiveChatBoxPreviews` (wires Chat* property-change subscriptions + first render); `CanAddYouTubeChat` (one-per-layout gate), `ShowChatInactiveMessage` (`!IsLive`). **Alerts (TASK 43):** same facade for the native alert box — `_alertLayer` (`AlertOverlayLayer`), `RenderAlertBox`/`UpdateAlertBoxPreview`, `UpdateActiveAlertBoxPreviews`, `CanAddAlerts` (one alert zone per layout). Ctor wiring (`MessageReceived` hookup, `new ChatBoxRenderer()`, `new AlertRenderer()`, `LoadLayout` preview start, `_youtubeChat.Dispose` + `_alertLayer.Dispose`) stays in core. |
|
||||
| `GoLiveViewModel.cs` | Start Stream dialog: **account row** (saved channel shown with Change Account, or Sign in to YouTube; Start gated on `IsSignedIn`/`IsBusy`) + title/description + **locked Visibility (always "Private" — ship step 7, no dropdown)**, start/cancel requests |
|
||||
|
||||
@@ -717,7 +717,7 @@ which also `ScheduleSave`s). Per raw frame a background worker (latest-frame-dro
|
||||
(`BlurRadius(0.5)` = 2, `BlurRadius(1)` = 6) and the behaviour at a step edge: default smears
|
||||
light ~9px into the dark half vs ~26px at full strength.
|
||||
|
||||
### Vertical (9:16) tier camera placement (TASK 57 — "vv" Unit 1, built 2026-10-21)
|
||||
### Vertical (9:16) tier camera placement (TASK 57 — "vv" Units 1+2, built 2026-10-21)
|
||||
|
||||
**The problem.** The vertical tier (1080×1920) is a centred **band crop** of the SAME 1920×1080
|
||||
master: `OutputRect*` = x 656.25, y 0, w 607.5, h 1080. A camera placed for 16:9 (the default
|
||||
@@ -732,7 +732,7 @@ exactly:
|
||||
|---|---|---|---|
|
||||
| 16:9 | as today | as today | as today |
|
||||
| **vv · Live** | circle **or** rect (the creator's choice, per scene) | small tile | one of **six** spots: top/bottom × left/centre/right |
|
||||
| vv · Chat | *Unit 2* (rect only; normal, or enlarged to one 607×540 area) | — | — |
|
||||
| vv · Chat | **rect only** (the circle option is not offered) | the small tile (normal), or enlarged to fill one 607×540 area | the same six spots; enlarged fills the top or the bottom area |
|
||||
| Starting / BRB / Ending | unchanged | unchanged | unchanged |
|
||||
|
||||
**Design — geometry only, no render-path change.** The preview and the compositor both read the
|
||||
@@ -761,6 +761,17 @@ spot — the corner the 16:9 default webcam uses. The Live scene gets that tile
|
||||
first time the vertical tier is picked (otherwise the vertical stream has no camera at all); the
|
||||
other scenes keep their 16:9 placement until the creator drags them into the band.
|
||||
|
||||
**The Chat scene's own rules (Unit 2).** In the vertical tier the Chat camera is **rect only**:
|
||||
the circle is not *offered* (`MainViewModel.CanToggleClipShape` is false → the Shape button
|
||||
disappears) and the model refuses it too — `WebcamSceneConfig.ClipShape` is **overridden**, so a
|
||||
locked camera cannot become Round by ANY writer (the toggle, the button, the placement swap). The
|
||||
lock is `WebcamSceneConfig.VerticalShapeLocked`, set by the VM per scene and cleared **before** the
|
||||
16:9 layout comes back, so the desktop shape is restored. Its **size is discrete**: the small tile
|
||||
(normal) or one whole 607×540 area (enlarged) — the resize handle snaps to the nearer of the two
|
||||
(`VerticalCameraLayout.SizeFor`), and an enlarged camera **fills the area its centre is in** (top
|
||||
or bottom). The Chat scene also gets its tile automatically the first time the vertical tier is
|
||||
picked (like Live) — otherwise the vertical chat stream has no camera at all.
|
||||
|
||||
**Manual entry** (creator rule): the webcam's Layers properties gained a **Geometry** section
|
||||
(X/Y/W/H) — it edits the LIVE placement, i.e. the one the current tier shows. The webcam's
|
||||
geometry setters clamp it into the band, so a typed value can never push the camera out of the
|
||||
@@ -773,11 +784,15 @@ built-in way to store separate layouts per aspect ratio, so you rely on manual w
|
||||
plugins" — streamyard.com/blog/dual-aspect-ratio-streaming-tool). Our per-scene, per-tier stored
|
||||
placement, resolved on the tier change, is the step past that.
|
||||
|
||||
**Tests.** `VerticalWebcamLayoutTests` — the ONE integration test for the unit: the vertical tier
|
||||
**Tests.** `VerticalWebcamLayoutTests` — the ONE integration test for Unit 1: the vertical tier
|
||||
is picked → the Live camera comes INTO the band as a small tile → dragged to the top-right it
|
||||
lands top-right inside the band → back to 16:9 the desktop layout is restored exactly → and both
|
||||
placements survive a save + reload. (The store writes the webcam rows only when the app has a
|
||||
webcam identity — the config rows hang off it — so the test supplies one.)
|
||||
`VerticalChatCameraLayoutTests` — the ONE integration test for Unit 2: the vertical tier is picked
|
||||
→ the Chat camera comes into the band as a small RECT tile → the circle option is not offered and
|
||||
cannot be set even on the model → dragged up and enlarged it fills the **top** area exactly → back
|
||||
to 16:9 the desktop layout is restored exactly and the circle option is offered again.
|
||||
|
||||
**Known trap (see `MyMistakes.md`):** a second properties panel bound to `SelectedElement.IsWebcam`
|
||||
broke `WebcamBackgroundPillTests` — that test finds the webcam's panel by **binding text**, so a
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TASK 57, the "vv" feature, Unit 2 — the vertical (9:16) tier's CHAT scene
|
||||
/// camera.
|
||||
///
|
||||
/// Unit 1 gave every scene its own vertical placement. Unit 2 is the Chat
|
||||
/// scene's own rules (the creator's table, 2026-10-17): in the vertical tier
|
||||
/// the Chat camera is **rect only — the circle option is not offered** — and
|
||||
/// its size is either the normal small tile or **enlarged to fill one whole
|
||||
/// 607×540 area** (the top one or the bottom one, by where the camera is).
|
||||
///
|
||||
/// One test, the whole story (the Good Dog rule): the vertical tier is picked →
|
||||
/// the Chat camera comes into the band as a small RECT tile → the circle option
|
||||
/// is not offered and cannot be set even by the model → the camera is dragged up
|
||||
/// and enlarged, and it fills the TOP area exactly → switching back to 16:9
|
||||
/// restores the desktop layout exactly (and the circle option is offered again).
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class VerticalChatCameraLayoutTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public VerticalChatCameraLayoutTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void Vertical_Tier_Chat_Camera_Is_Rect_Only_And_Enlarges_To_One_Area()
|
||||
{
|
||||
_app.Run(Run);
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-vv-chat-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
try
|
||||
{
|
||||
var vm = new MainViewModel();
|
||||
|
||||
var chat = vm.Scenes.First(s => s.Name == "Chat");
|
||||
var camera = new WebcamSceneConfig
|
||||
{
|
||||
WebcamId = "webcam-1",
|
||||
Name = "Webcam",
|
||||
X = 120,
|
||||
Y = 120,
|
||||
Width = 640,
|
||||
Height = 360,
|
||||
};
|
||||
chat.Elements.Add(camera);
|
||||
|
||||
// The 16:9 placement: the camera sits where the desktop layout put it.
|
||||
Assert.Equal(120, camera.X);
|
||||
Assert.Equal(120, camera.Y);
|
||||
Assert.Equal(640, camera.Width);
|
||||
|
||||
// ── The vertical tier is picked ───────────────────────────────────
|
||||
vm.SelectedQuality = vm.QualityOptions[^1];
|
||||
Assert.True(vm.IsVerticalTier);
|
||||
var band = vm.VerticalBand;
|
||||
|
||||
// The Chat camera came INTO the band as a small tile...
|
||||
Assert.Equal(VerticalCameraLayout.TileWidth, camera.Width);
|
||||
Assert.Equal(VerticalCameraLayout.TileHeight, camera.Height);
|
||||
Assert.True(camera.X >= band.Left, $"camera X {camera.X} is left of the band {band.Left}");
|
||||
Assert.True(camera.X + camera.Width <= band.Right, "the camera hangs over the band's right edge");
|
||||
Assert.True(camera.Y >= band.Top, "the camera hangs over the band's top edge");
|
||||
Assert.True(camera.Y + camera.Height <= band.Bottom, "the camera hangs over the band's bottom edge");
|
||||
|
||||
// ── The circle option is not offered ─────────────────────────────
|
||||
vm.SelectedElement = camera;
|
||||
Assert.False(vm.CanToggleClipShape);
|
||||
Assert.Equal(ClipShape.Traditional, camera.ClipShape);
|
||||
|
||||
// ...and it cannot be slipped in behind the UI's back either.
|
||||
camera.ToggleClipShape();
|
||||
Assert.Equal(ClipShape.Traditional, camera.ClipShape);
|
||||
|
||||
// ── Drag it up, then enlarge it: it fills the TOP area ──────────
|
||||
vm.MoveWebcamInVerticalTier(camera, band.Left + 40, band.Top + 40);
|
||||
vm.ResizeWebcamInVerticalTier(camera, band.Width, band.Height);
|
||||
|
||||
Assert.Equal(band.Left, camera.X, 3);
|
||||
Assert.Equal(band.Top, camera.Y, 3);
|
||||
Assert.Equal(band.Width, camera.Width, 3);
|
||||
Assert.Equal(band.Height / 2, camera.Height, 3);
|
||||
|
||||
// ── Back to 16:9: the desktop layout is restored exactly ─────────
|
||||
vm.SelectedQuality = vm.QualityOptions[0];
|
||||
Assert.False(vm.IsVerticalTier);
|
||||
Assert.Equal(120, camera.X);
|
||||
Assert.Equal(120, camera.Y);
|
||||
Assert.Equal(640, camera.Width);
|
||||
Assert.Equal(360, camera.Height);
|
||||
|
||||
// The circle option is offered again outside the vertical tier.
|
||||
Assert.True(vm.CanToggleClipShape);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user