feat: выбор граней окон для солнечных лучей (#577)

Issue: #577
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-15 00:16:35 +03:00
parent 949422fe91
commit db4b397e2e
70 changed files with 1209 additions and 885 deletions
+6 -1
View File
@@ -263,8 +263,13 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
* отдельный модуль; механизм обязан быть синхронным на первом касании и потому
* не может жить в lazy editor graph. Измеренный факт 290 181 Б оставляет 719 Б
* сверху и 1 281 Б до нижней границы полосы.
*
* 2026-09-15, #577: потолок перецентрирован 290 900 → 291 300 без изменения
* общего бюджета. Выбор внутренней или внешней грани окна относится к
* синхронной геометрии лучей в initial View. Измеренный факт 290 629 Б
* оставляет 671 Б сверху и 1 329 Б до нижней границы полосы.
*/
export const INITIAL_VIEW_GZIP_CEILING = 290_900;
export const INITIAL_VIEW_GZIP_CEILING = 291_300;
export const INITIAL_VIEW_CEILING_BAND = 2_000;
/**
+15
View File
@@ -7,6 +7,21 @@
* field whose key is still current but whose old value is compatibility-only.
*/
export const CONFIG_FIELD_REGISTRY = Object.freeze([
{
id: 'settings.sun_ray_origin',
enforcedBy: 'sun.ts + validation.py + config-schema parity; #577',
selector: { path: ['settings', 'sun_ray_origin'] },
storage: 'House Plan server config',
type: "'inner' | 'outer'",
default: "unset (= 'inner', the pre-#577 geometry)",
level: 'global',
ui: 'General settings → Sun → Sun rays',
runtime: 'selects the physical window face used as the source span',
introduced: '#577',
status: 'current',
migration: "none — old configs stay on 'inner'; new settings saves materialise the enum",
compatibility: "unknown values fail backend writes and fall back to 'inner' in the frontend",
},
{
id: 'settings.summary_panel',
enforcedBy: 'summary-panel.ts + validation.py change-aware reference validation; #437',
+7
View File
@@ -442,6 +442,13 @@
}
]
},
"config.settings.sun_ray_origin": {
"enum": [
"inner",
"outer"
],
"required": false
},
"config.settings.sun_rays": {
"required": false,
"type": "bool"
+57
View File
@@ -5329,6 +5329,63 @@ const MUTANT_DEFINITIONS = [
replace: "{ id: 'openings-filled-tunnel-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',\n fillMode: 'none', customFill: { c: '#66717c', a: 0.55 }, glowEnabled: false,",
}],
},
{
id: 'sun-ray-origin-outer-ignored',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="#577: outer rays" test/sun.test.mjs',
because: 'если внешний режим снова использует внутреннюю грань, геометрия остаётся '
+ 'визуально прежней; unit-защита #577 обязана увидеть обе фасадные точки и тоннель',
patches: [{
file: 'src/sun.ts',
find: " const side = origin === 'outer' ? -1 : 1;",
replace: ' const side = 1; // mutant: ignore outer source face',
}],
},
{
id: 'sun-ray-origin-cache-ignored',
guard: 'node demo/smoke_sun.mjs',
because: 'переключение в открытом диалоге не меняет config epoch; без origin в ключе '
+ 'пользователь до следующей записи видит закэшированную старую геометрию (#577)',
patches: [{
file: 'src/houseplan-card.ts',
find: '`${space.id}|${sun.azimuth}|${sun.elevation}|${north}|${origin}|${this._cfgEpoch}`',
replace: '`${space.id}|${sun.azimuth}|${sun.elevation}|${north}|${this._cfgEpoch}`',
}],
},
{
id: 'sun-ray-origin-save-forced-inner',
guard: 'node demo/smoke_sun.mjs',
because: 'селектор без сохранения — ложная функция; браузерный round-trip обязан '
+ 'увидеть outer после Save и при повторном открытии общих настроек (#577)',
patches: [{
file: 'src/houseplan-editor-runtime.ts',
find: ' settings.sun_ray_origin = d.sunRayOrigin;',
replace: " settings.sun_ray_origin = 'inner'; // mutant: discard the selected face",
}],
},
{
id: 'sun-ray-origin-backend-allows-any-string',
guard: 'node scripts/backend-test-guard.mjs test_sun_settings_global tests_backend/test_validation.py',
because: 'backend обязан отвергать неизвестный enum, иначе сохранённая опечатка расходится '
+ 'с UI и молча читается как inner; строгий validation-тест #577 должен покраснеть',
patches: [{
file: 'custom_components/houseplan/validation.py',
find: '_SUN_RAY_ORIGIN = vol.In(["inner", "outer"])',
replace: '_SUN_RAY_ORIGIN = str # mutant: accept any string',
}],
},
{
id: 'sun-ray-origin-support-leaks-invalid',
guard: 'node scripts/backend-test-guard.mjs '
+ 'test_projection_helpers_fail_closed_on_malformed_shapes tests_backend/test_support_package.py',
because: 'support package переносит только безопасный известный enum; неизвестная строка '
+ 'не должна попадать в диагностический архив (#577)',
patches: [{
file: 'custom_components/houseplan/support_package.py',
find: ' if sun_ray_origin in ("inner", "outer"):',
replace: ' if isinstance(sun_ray_origin, str): # mutant: leak unknown strings',
}],
},
{
id: 'sun-north-subtraction-restored',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '