mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
feat: режим «Значение + статичный значок» (#588)
Пятый вариант «Отображение»: маркер показывает выбранное значение и при этом
никогда не меняет цвет — ни по состоянию, ни по тревоге, ни по недоступности,
ни под цвет RGB-света, ни по активности.
Четыре прежних режима задавали одним выбором две независимые вещи: что
нарисовано внутри маркера и красится ли он состоянием. Поэтому вместо
сравнения с одним токеном появились два производных предиката рядом с
`normalizeDeviceDisplay` — `displayWantsValue` и `displayIsNeutral`, — и их
спрашивают политика, слой пульсации, внешний бейдж, редактор и три ветки
живого слоя пылесоса.
Две ловушки, из-за которых режим не сводится к одной строке в словаре:
- быстрый путь «статичному маркеру источники не нужны» (`sourceDetails: false`)
— это основной путь рендера плана, карточки пространства и PDF. Он оставлен
только режиму без значения: иначе число пропало бы именно на плане и
осталось в предпросмотре редактора;
- три ветки живого пылесоса сравнивают режим строкой и не читают политику,
поэтому зелёная политика их не гарантирует. Свидетель держит две
конфигурации карты: сопоставленная доказывает puck и след, несопоставленная
— бейдж маршрута (при совпадающей калибровке маршрут `ready`, и бейджа не
было бы ни в одном режиме). Бейдж проверяется при наполненном буфере
позиций, иначе его отсутствие объяснялось бы первой веткой.
Потолок initial View перецентрирован 291_700 → 292_400 без изменения общего
бюджета: измеренный факт 291 346 Б оставлял под прежним центром 354 Б —
внутри шумовой полосы метрики.
Эталон: в `device-icon-state-table-{light,dark}` включённая RGB-лампа
переведена в новый режим. Кадры обязаны разойтись; приёмка — отдельным
коммитом класса D с полного линуксового артефакта Validate.
Issue: #588
User-Visible: yes
This commit is contained in:
@@ -282,8 +282,16 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
|
||||
* синхронный View до первого движущегося кадра; общий renderer бумажных фигур
|
||||
* удержал объединённый факт на 291 058 Б — 642 Б сверху и 1 358 Б до нижней
|
||||
* границы полосы.
|
||||
*
|
||||
* 2026-09-18, #588: потолок перецентрирован 291_700 → 292_400 без изменения
|
||||
* общего бюджета. Пятый режим «Отображение» — это два производных предиката
|
||||
* вместо одной проверки токена и пятая строка в двух словарях ярлыков; всё это
|
||||
* синхронный View, потому что режим решает, что нарисовано внутри маркера при
|
||||
* первом кадре. Факт на `dev` был 291 156 Б, измеренный факт 291 346 Б (+190 Б)
|
||||
* оставлял под прежним центром 354 Б сверху — внутри шумовой полосы метрики.
|
||||
* Новый центр даёт 1 054 Б сверху и 946 Б до нижней границы полосы.
|
||||
*/
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 291_700;
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 292_400;
|
||||
export const INITIAL_VIEW_CEILING_BAND = 2_000;
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
{
|
||||
"const": "static_icon"
|
||||
},
|
||||
{
|
||||
"const": "value_static_icon"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
|
||||
@@ -6035,8 +6035,10 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'metrics, pulse and vacuum live together rather than relying on renderer-specific checks',
|
||||
patches: [{
|
||||
file: 'src/device-presentation-policy.ts',
|
||||
find: ' } else if (staticIcon) {\n visual = NEUTRAL_VISUAL;',
|
||||
replace: ' } else if (false && staticIcon) {\n visual = NEUTRAL_VISUAL;',
|
||||
// #588 переименовал переменную: она отвечает уже не за один токен, а за
|
||||
// «нейтральное лицо» обоих статичных режимов. Якорь переехал, смысл нет.
|
||||
find: ' } else if (neutralFace) {\n visual = NEUTRAL_VISUAL;',
|
||||
replace: ' } else if (false && neutralFace) {\n visual = NEUTRAL_VISUAL;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -6100,7 +6102,9 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'single gate either hides valid values or revives ambiguous arbitrary registry rows',
|
||||
patches: [{
|
||||
file: 'src/device-presentation-policy.ts',
|
||||
find: " const face: PresentationFace = input.display === 'value'\n"
|
||||
// #588: лицо значения теперь спрашивают предикатом, а не сравнением с
|
||||
// одним токеном — режимов со значением стало два. Якорь переехал.
|
||||
find: ' const face: PresentationFace = wantsValue\n'
|
||||
+ " && !effectiveHidden && input.valueAvailable ? 'value' : 'icon';",
|
||||
replace: " const face: PresentationFace = false\n"
|
||||
+ " && !effectiveHidden && input.valueAvailable ? 'value' : 'icon';",
|
||||
@@ -6115,7 +6119,9 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'satellites escape a static face even though the core itself remains neutral',
|
||||
patches: [{
|
||||
file: 'src/device-presentation-policy.ts',
|
||||
find: ' const metrics = !staticIcon && !effectiveHidden;',
|
||||
// #588: переменная переименована в `neutralFace` — режимов с нейтральным
|
||||
// лицом стало два. Якорь переехал, смысл прежний.
|
||||
find: ' const metrics = !neutralFace && !effectiveHidden;',
|
||||
replace: ' const metrics = true;',
|
||||
}],
|
||||
},
|
||||
@@ -10043,6 +10049,90 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " if (candidateTag && isStableTag(candidateTag) && candidateTag === 'v0.0.0-mutant') {",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'value-static-icon-keeps-live-colour',
|
||||
guard: 'node --test test/device-presentation-policy.test.mjs',
|
||||
because: '#588 AC1: «никогда не меняет цвет» — весь смысл режима. Стоит нейтральности '
|
||||
+ 'снова стать свойством одного токена, и маркер начнёт краснеть тревогой, гаснуть '
|
||||
+ 'недоступностью и брать цвет RGB-лампы — молча, потому что значение при этом видно',
|
||||
patches: [{
|
||||
file: 'src/device-presentation-policy.ts',
|
||||
find: ' const neutralFace = displayIsNeutral(input.display);',
|
||||
replace: " const neutralFace = input.display === 'static_icon';",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'value-static-icon-loses-value-fallback',
|
||||
guard: 'node --test test/device-presentation.test.mjs',
|
||||
because: '#588 AC2: когда значение получить нельзя, лицо откатывается к значку, и '
|
||||
+ 'причина отката — единственное, что объясняет человеку, почему числа нет. Потеря '
|
||||
+ 'причины оставляет пустой значок без объяснения ровно в новом режиме',
|
||||
patches: [{
|
||||
file: 'src/device-presentation.ts',
|
||||
find: ' fallbackReason: displayWantsValue(display) ? value.fallback : null,',
|
||||
replace: " fallbackReason: display === 'value' ? value.fallback : null,",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'value-static-icon-takes-the-sourceless-fast-path',
|
||||
guard: 'node --test test/device-presentation.test.mjs',
|
||||
because: '#588 AC3: быстрый путь «статичному маркеру источники не нужны» действует на '
|
||||
+ 'основном пути рендера плана. Распространить его на режим со значением — значит '
|
||||
+ 'потерять число именно на плане и сохранить его в предпросмотре редактора',
|
||||
patches: [{
|
||||
file: 'src/device-presentation.ts',
|
||||
find: " const sourceless = display === 'static_icon';",
|
||||
replace: ' const sourceless = neutralFace;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'value-static-icon-pulses-on-alarm',
|
||||
guard: 'node --test test/device-pulse.test.mjs',
|
||||
because: '#588 AC4: тревога пробивает и выключенные живые состояния, и отсутствие '
|
||||
+ 'активности. Если пульсация снова принадлежит одному токену, «маркер не меняет цвет» '
|
||||
+ 'перестаёт быть правдой в самый заметный момент',
|
||||
patches: [{
|
||||
file: 'src/device-pulse.ts',
|
||||
find: ' || displayIsNeutral(display)) {',
|
||||
replace: " || display === 'static_icon') {",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'value-static-icon-keeps-live-vacuum',
|
||||
guard: 'node demo/smoke_static_icon.mjs',
|
||||
because: '#588 AC6 (К2а): три ветки живого слоя пылесоса сравнивают режим строкой и не '
|
||||
+ 'читают политику, поэтому зелёный AC1 их не гарантирует. Возвращённые к одному токену, '
|
||||
+ 'они оставляют над «неизменным» маркером едущий puck, след и бейдж маршрута',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " if (displayIsNeutral(normalizeDeviceDisplay(d.marker?.display))) {\n this._vacRt.delete(d.id);",
|
||||
replace: " if (normalizeDeviceDisplay(d.marker?.display) === 'static_icon') {\n this._vacRt.delete(d.id);",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'value-static-icon-keeps-vacuum-overlay',
|
||||
guard: 'node demo/smoke_static_icon.mjs',
|
||||
because: '#588 AC6 (К2а): вторая из трёх веток — рендер puck и следа. Она не зависит от '
|
||||
+ '`vacuumLive` политики, и её одной достаточно, чтобы над нейтральным маркером ездил '
|
||||
+ 'робот',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' if (displayIsNeutral(normalizeDeviceDisplay(d.marker?.display))) continue;',
|
||||
replace: " if (normalizeDeviceDisplay(d.marker?.display) === 'static_icon') continue;",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'value-static-icon-keeps-route-warning',
|
||||
guard: 'node demo/smoke_static_icon.mjs',
|
||||
because: '#588 AC6 (К2а): третья ветка — бейдж предупреждения о маршруте. Он появляется '
|
||||
+ 'только на несопоставленной карте, поэтому свидетель держит две конфигурации: при '
|
||||
+ 'совпадающей калибровке маршрут `ready` и бейджа нет ни в одном режиме',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' if (displayIsNeutral(normalizeDeviceDisplay(d.marker?.display))) return nothing;',
|
||||
replace: " if (normalizeDeviceDisplay(d.marker?.display) === 'static_icon') return nothing;",
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8');
|
||||
|
||||
Reference in New Issue
Block a user