Phase 11b: fill свободных camera-cells остальными drawable камерами

User: "смущает чёрная ячейка в сетке". Причина: asymmetric templates
имели widget cells (placeholder тёмно-серый Y=40) + при недостатке active
camera cells оставались BlankCell (чёрный).

Два изменения:

1. templates.json — оставили только 16:9 layouts (tpl_1/tpl_4/tpl_9/tpl_16).
   Все camera-cells, никаких widget-областей. Cells full 16:9 (cs==rs
   микроячейки), полностью покрывают output 1920×1080 без чёрных полос.
   Asymmetric layouts (main + satellites) удалены — вернуть в Phase 12
   когда widget'ы будут реальными (HA-chat, temperature graph).

2. composer::maybe_relayout — заполнить свободные camera-cells остальными
   drawable камерами из pool (по priority), если template имеет больше
   cells чем motion-active. Условие: cap > active.size().

Производство при 4 источниках в pool:
  - 1 motion → tpl_1 (1 cell full screen)
  - 2 motion → tpl_4 (2 motion + 2 not-active drawable = 4 cells заняты)
  - 4 motion → tpl_4 (все 4 motion)
  - При добавлении новых камер (до 16) — tpl_9 при 5..9, tpl_16 при 10..16

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 09:15:20 +01:00
parent 6e0273f4b4
commit 9d2a0b2bd7
2 changed files with 51 additions and 58 deletions
+31 -58
View File
@@ -2,7 +2,7 @@
"version": 1,
"grid_cols": 8,
"grid_rows": 8,
"_doc": "Layout-templates для cfc-grid auto-layout. Координаты в микроячейках 8×8 (output 1920×1080 → каждая микроячейка 240×135 px, 16:9). Квадраты N×N микроячеек тоже 16:9. role=camera — заполняется из активных камер по priority. role=widget — placeholder.",
"_doc": "Phase 11b — только пропорциональные 16:9 layouts (cs == rs микроячеек). Полностью заполняют output 1920×1080 без widget-областей. При недостатке active-камер по motion свободные camera-cells заполняются остальными drawable-камерами из pool (по priority). См. cfc::Composer::maybe_relayout().",
"templates": [
{
@@ -12,20 +12,9 @@
{"col": 0, "row": 0, "cs": 8, "rs": 8, "role": "camera", "order": 0}
]
},
{
"name": "tpl_3",
"_desc": "Главная 1440×810 слева + 2 превью 480×270 справа стопкой, остаток — виджеты.",
"cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 4, "role": "widget", "widget": "temp_chart"},
{"col": 0, "row": 6, "cs": 6, "rs": 2, "role": "widget", "widget": "ha_chat"}
]
},
{
"name": "tpl_4",
"_desc": "Quad 2×2: 4 камеры 960×540. order=0 — top-left (главная).",
"_desc": "Quad 2×2 4 камеры 960×540 (16:9). order=0 — top-left (главная).",
"cells": [
{"col": 0, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 0},
{"col": 4, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 1},
@@ -34,56 +23,40 @@
]
},
{
"name": "tpl_5",
"_desc": "1 главная + 4 превью справа стопкой, нижняя полоса — виджет.",
"name": "tpl_9",
"_desc": "3×3 grid — 9 камер... только cells 2×2 микроячейки по 6×6 области. ВНИМАНИЕ: 8×8 не делится на 3 без остатка; используем 6×6 камер + bottom/right остаток как background. Если потом будут asymmetric с widget'ами — выделим Phase 12.",
"cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 0, "row": 6, "cs": 6, "rs": 2, "role": "widget", "widget": "ha_chat"}
{"col": 0, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 0},
{"col": 2, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 4, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 0, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 2, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 4, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 0, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 2, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 7},
{"col": 4, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 8}
]
},
{
"name": "tpl_6",
"_desc": "1 главная + 3 правые + 2 нижние, остаток — виджет.",
"name": "tpl_16",
"_desc": "4×4 grid — 16 камер по 480×270 (16:9). Полностью покрывает 8×8 без остатка.",
"cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 6, "cs": 4, "rs": 2, "role": "widget", "widget": "info"}
]
},
{
"name": "tpl_7",
"_desc": "1 главная + 3 правые + 3 нижние, угол — виджет.",
"cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "widget", "widget": "ha_chat"}
]
},
{
"name": "tpl_8",
"_desc": "1+3+4 — главная + 3 правые + полная нижняя строка.",
"cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 7}
{"col": 0, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 0},
{"col": 2, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 4, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 7},
{"col": 0, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 8},
{"col": 2, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 9},
{"col": 4, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 10},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 11},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 12},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 13},
{"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 14},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 15}
]
}
]
+20
View File
@@ -215,6 +215,26 @@ void Composer::maybe_relayout()
int cap = tpl->nb_camera_cells();
if (static_cast<int>(active.size()) > cap) active.resize(cap);
/* Если template имеет больше camera-cells чем активных по motion —
* заполнить оставшиеся drawable камерами из pool (по priority),
* которые ещё не вошли в active. Это убирает "чёрные ячейки"
* в asymmetric layouts (tpl_3/5/6/7 + tpl_4 при active<4). */
if (static_cast<int>(active.size()) < cap) {
std::vector<PoolEntry*> already(active.begin(), active.end());
std::vector<PoolEntry*> extras;
const_cast<SourcePool&>(pool_).for_each([&](PoolEntry& e) {
if (!e.drawable()) return;
for (auto* a : already) if (a == &e) return;
extras.push_back(&e);
});
std::sort(extras.begin(), extras.end(),
[](PoolEntry* a, PoolEntry* b) { return a->priority > b->priority; });
for (auto* e : extras) {
if (static_cast<int>(active.size()) >= cap) break;
active.push_back(e);
}
}
std::string sig = build_signature(tpl->name, active);
if (sig == committed_signature_) {