Files
houseplan-card/docs/specs/166-sun-north-rotation.md
T
2026-08-17 11:55:38 +03:00

353 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #166 — солнечные лучи зеркально учитывают направление севера
Статус: **ТЗ принято, готово к реализации**
Дата: 2026-08-16
Тип: `bug` · приоритет: `P1` · user value: 8/10 · complexity: 4/10 · risk: 6/10
Issue: [#166](https://github.com/Matysh/houseplan-card/issues/166)
Ветка: `issue/166-sun-north-rotation`
Канонические документы: [SCOPE](../SCOPE.md), [SUN](../SUN.md),
[USER-GUIDE](../USER-GUIDE.md), [USER-GUIDE.ru](../USER-GUIDE.ru.md),
[TOUCH-SUPPORT](../TOUCH-SUPPORT.md), [TESTING](../TESTING.md),
[CONFIG-COMPATIBILITY](../CONFIG-COMPATIBILITY.md).
## 1. Сценарий и продуктовый контекст
Основная персона — владелец дома, который настроил план по реальной ориентации
здания и ежедневно смотрит его в Full View либо на kiosk-панели. Он поворачивает
стрелку N в настройках туда, где на плане действительно находится север, и
ожидает, что оконные лучи будут соответствовать текущему положению Солнца.
Это прямой контракт SCOPE J1: House Plan должен правдиво показывать текущее
состояние дома. Выбор окна с неправильной стороны здания является фактически
ложной визуализацией, даже если сам эффект декоративный.
## 2. Что человек увидит до и после
**До:** если истинный север не совпадает с верхом холста, поворот стрелки N
зеркально поворачивает солнечное направление. Чтобы осветилось физически верное
окно, пользователю приходится намеренно ставить компас не по реальному северу.
При `north_deg = 0` ошибка незаметна.
**После:** стрелка N буквально указывает истинный север на плане. House Plan
комбинирует это направление с азимутом `sun.sun`, выбирает окно на физически
правильной стороне и ведёт луч от него внутрь комнаты. Ручная зеркальная
компенсация больше не нужна.
## 3. Подтверждённая причина
1. `_compassPoint()` в `src/houseplan-card.ts` вычисляет угол через
`atan2(dx, -dy)`: верх холста = 0°, право = 90°, низ = 180°, лево = 270°.
2. `_renderCompass()` поворачивает стрелку N на сохранённый `north_deg`, поэтому
UI показывает именно направление истинного севера на холсте.
3. `docs/SUN.md` определяет `north_deg` тем же образом: градусы по часовой
стрелке от верха холста до истинного севера.
4. `planSunAngle()` в `src/sun.ts` вопреки этому вычисляет
`norm360(azimuth - northDeg)`. Вычитание отражает поворот относительно
нулевой оси; для заявленной семантики направления должны складываться.
5. `test/sun.test.mjs` закрепляет неверный знак ожиданием «east sun +
`north_deg=90` → up» и выбором верхнего окна.
6. `demo/fixtures/visual-matrix.mjs` использует `north_deg = 0`, поэтому
действующие golden-сцены не различают сложение и вычитание.
Репорт подтверждён по коду и является дефектом координатного преобразования.
## 4. Координатный контракт
Все углы нормализуются в `[0, 360)` и растут по часовой стрелке.
- `A` (`azimuth`) — направление от истинного севера к Солнцу из `sun.sun`:
N=0°, E=90°, S=180°, W=270°.
- `N` (`north_deg`) — направление истинного севера на холсте: верх=0°,
право=90°, низ=180°, лево=270°.
- `P` — направление **к Солнцу** на холсте.
Каноническая формула:
```text
P = norm360(N + A)
toSun = (sin(P), -cos(P))
awayFromSun = -toSun
```
Здесь ось X растёт вправо, ось Y — вниз. `toSun` используется для проверки,
какая внешняя нормаль окна смотрит на Солнце; `awayFromSun` остаётся
направлением хода света от окна внутрь комнаты.
Контрольные примеры:
| Север на холсте N | Азимут A | Направление к Солнцу P | Сторона холста |
|---:|---:|---:|---|
| 0° (вверх) | 90° (восток) | 90° | справа |
| 90° (вправо) | 0° (север) | 90° | справа |
| 90° (вправо) | 90° (восток) | 180° | снизу |
| 270° (влево) | 90° (восток) | 0° | сверху |
| 350° | 20° | 10° | wrap через 360° |
## 5. Scope
В задачу входят:
1. исправление преобразования `azimuth + north_deg` в единственной чистой
функции солнечного направления;
2. сохранение действующего контракта вектора к Солнцу и противоположного
вектора луча внутрь комнаты;
3. исправление unit-ожиданий, которые сейчас закрепляют неверный знак;
4. smoke-проверка выбора окон при ненулевом global `north_deg` и per-space
override;
5. детерминированная golden-сцена с ненулевым севером, чтобы знак поворота был
визуально различим;
6. исправление формулы и примеров в `docs/SUN.md` и пользовательских руководствах;
7. предупреждение о ранее компенсированных настройках в обоих changelog;
8. обычные bundle/release-артефакты пользовательского исправления.
## 6. Non-scope
В задачу не входят:
- изменение значений либо частоты обновления `sun.sun`;
- новая астрономическая модель, геолокация или расчёт положения Солнца внутри
House Plan;
- изменение длины, цвета, opacity, fade, rim или clipping солнечных лучей;
- изменение определения внешнего окна либо взаимное затенение крыльями здания;
- изменение четырёхфазного фона `daynight`, который по контракту #146 не
зависит от компаса;
- изменение UI компаса, его диапазона, шага, inheritance или доступности;
- изменение режима редакторов или добавление лучей в статическую
`houseplan-space-card`;
- автоматическая миграция либо эвристическое распознавание пользовательской
компенсации старого дефекта.
## 7. Функциональное поведение
### 7.1. Преобразование направления
`planSunAngle(azimuth, northDeg)` возвращает
`norm360(azimuth + northDeg)`. Для любых конечных входов сохраняется текущая
нормализация угла. `sunDirOnPlan()` продолжает возвращать единичный вектор к
Солнцу в координатах холста.
Архитектурный инвариант для текущих и будущих потребителей, включая
SUN-CONTRAST: направление Солнца на плане берётся только из
`planSunAngle()`. Ни render-слой, ни contrast-логика не вводят вторую формулу,
зеркальное преобразование или дополнительную поправку знака.
Знак меняется только в композиции двух систем координат. Нельзя одновременно
инвертировать `toSun`, нормали окон или `awayFromSun`: это дало бы локально
правильный тест, но снова перепутало бы освещённую сторону либо направление
луча внутри комнаты.
### 7.2. Выбор окна и геометрия луча
`windowLit()` продолжает сравнивать внешнюю нормаль окна с `toSun`. После
исправления выбирается окно со стороны холста, соответствующей `P`. Геометрия
луча продолжает строиться вдоль `-toSun`, то есть от освещённого окна внутрь
принимающей комнаты. Все действующие правила exterior/interior, wall depth,
room clipping, physical obstacles, gradient и rim сохраняются.
Для контрольного плана с окнами на четырёх сторонах, `north_deg=90` и
`azimuth=90` должно выбираться нижнее окно холста, а не верхнее. Луч от нижнего
окна идёт вверх, внутрь комнаты.
### 7.3. Global и per-space
Правило наследования не меняется: явный `north_deg` пространства выигрывает у
global; пустой override наследует global; отсутствие обоих отключает оконные
лучи. И global, и override используют одну исправленную формулу. Preview
несохранённого значения в открытом диалоге должен давать тот же результат,
что и значение после сохранения.
### 7.4. Гейты и соседние режимы
Без изменений остаются:
- отсутствие/невалидность `sun.sun` выключает оконные лучи;
- `elevation <= 0` не создаёт геометрию, а 3°-порог и двухсекундный fade
продолжают действовать по текущему контракту;
- `sun_rays=false`, editor mode и отсутствие `north_deg` выключают слой;
- `bg_mode=daynight` и декоративный свет #146 не используют `north_deg`;
- memo key уже содержит `azimuth`, `elevation`, effective north и config epoch;
его состав менять не требуется.
## 8. UX, accessibility, touch и i18n
Компас, number input, 1°/15° шаги, pointer capture, клавиатурный ввод и
подписи остаются прежними. Исправляется результат существующей настройки, а
не interaction.
Full View и kiosk сохраняют полный touch-контракт. Настройка компаса остаётся
desktop-first поверхностью редактора настроек; новых жестов или hover-only
действий нет. Новых i18n-ключей не требуется. EN/RU пользовательская
документация должна однозначно сказать, что стрелка N направляется туда, где
на плане находится истинный север.
## 9. Данные, migration и совместимость
Схема не меняется: `north_deg` остаётся integer 0–359 на global и per-space
уровнях. Backend validation, storage version, export/import и default `null`
не меняются.
Автомиграции `north_deg -> 360 - north_deg` нет. Она повредила бы корректно
сохранённые по документированному смыслу значения и не может отличить их от
намеренной компенсации. После обновления:
- значения, выставленные по реальному северу, начинают давать правильные лучи;
- `0°` и `180°` визуально сохраняют прежнее направление;
- пользователю, который зеркально компенсировал дефект, нужно один раз вернуть
стрелку N к реальному северу. Это явно указывается в EN/RU changelog.
## 10. Acceptance criteria и доказательства
### AC-01 — математический контракт
`planSunAngle()` использует сложение и нормализацию. Таблица из раздела 4,
включая wrap и некардинальный угол, проходит в `test/sun.test.mjs`.
**Доказательство:** unit-тесты точных углов и `sunDirOnPlan()` с допуском для
float.
### AC-02 — семантика компаса совпадает с геометрией
Положения стрелки N сверху/справа/снизу/слева означают соответственно
0/90/180/270°, а одинаковое значение, заданное dial либо number input, даёт
одинаковое направление лучей до и после сохранения.
**Доказательство:** действующие compass smoke-проверки плюс направленный smoke
с `north_deg=90`.
### AC-03 — освещается физически правильное окно
На плане с четырьмя внешними окнами для `north_deg=90`, `azimuth=90` и
положительной elevation выбирается нижнее окно холста; луч направлен от него
внутрь. Внутреннее окно не участвует.
**Доказательство:** unit `computeSunRays()` и production-bundle
`demo/smoke_sun.mjs`.
### AC-04 — inheritance исправлен на обоих уровнях
Global north и per-space override используют новый знак; удаление override
снова применяет global. Отсутствие обоих по-прежнему не рисует лучи.
**Доказательство:** unit наследования и smoke текущей full-card конфигурации.
### AC-05 — соседнее поведение не изменено
Кардинальные направления при `north_deg=0`, night/elevation gates, exterior
window selection, clipping, wall thickness, obstacle subtraction, ray length,
fade/rim, editor gating и независимый four-phase background проходят
существующие тесты без ослабления assertions.
**Доказательство:** полный unit-набор и существующие sun smoke; отдельная
проверка, что изменение `north_deg` не меняет resolved day-cycle #146.
### AC-06 — визуальная регрессия больше не маскируется
В `GOLDEN_SCENARIOS` есть хотя бы одна детерминированная сцена с ненулевым
`north_deg` и асимметричным сочетанием азимута/окон, где ошибочный знак выбрал
бы другую сторону плана. Добавление либо семантическое изменение этой сцены
сопровождается повышением `GOLDEN_MATRIX_VERSION`.
**Доказательство:** обновлённый manifest/diff golden; ручное принятие только
изменённой ожидаемой сцены.
### AC-07 — документация и release contract
`docs/SUN.md` содержит формулу `norm360(north_deg + azimuth)`, а EN/RU guide
объясняет буквальную ориентацию стрелки N. Оба changelog называют исправление
и предупреждают пользователей старой ручной компенсации.
**Доказательство:** docs diff и terminal trailers `Issue: #166`,
`User-Visible: yes` в продуктовых коммитах реализации.
### AC-08 — performance, security и поддерживаемые поверхности
Исправление не добавляет DOM, таймеров, сетевых вызовов, config writes либо
новых пересчётов; геометрия по-прежнему memoized. View/kiosk работают на
desktop и touch; security boundary не меняется.
**Доказательство:** typecheck/unit/build в цикле, штатный performance gate и
smoke/golden перед бетой, без новых security exceptions.
## 11. План тестирования
### В цикле реализации
1. `typecheck`.
2. Полный unit-набор, включая обновлённый `test/sun.test.mjs`.
3. Production build.
Точные команды берутся из актуального `package.json`/runbook; набор не
расширяется golden/smoke/performance на каждой итерации.
### Перед бетой
1. `demo/smoke_sun.mjs` и связанные sun smoke.
2. Golden matrix с новой ненулевой ориентацией и review diff.
3. Штатный performance gate, чтобы подтвердить отсутствие нового churn.
4. Остальные golden/smoke/performance проверки по release runbook.
Полный HA harness на Windows не является каноном из-за `fcntl`; его
обязательное доказательство остаётся в Linux CI.
### Мутационный гейт
Перед передачей реализации на код-ревью автор вручную применяет каждый мутант,
запускает указанный узкий тест и фиксирует, что тест красный; после возврата
рабочей реализации тот же тест должен быть зелёным.
| Мутант | Обязательное доказательство |
|---|---|
| Вернуть в `planSunAngle()` вычитание `azimuth - northDeg` | Табличный unit AC-01 падает хотя бы на контрольных сочетаниях `90° + 90°` и wrap через 360° |
| Оставить сложение углов, но инвертировать `toSun` вместо исправления требуемой семантики вектора | Таблица углов AC-01 остаётся зелёной, а направленный unit AC-03 выбора нижнего окна при `north_deg=90`, `azimuth=90` падает |
| Вернуть `north_deg: 0` в новой golden-сцене | Структурный тест golden matrix падает, потому что обязательная запись в `GOLDEN_SCENARIOS` больше не содержит ненулевой ориентации, а `GOLDEN_MATRIX_VERSION` не соответствует сценарию |
Гейт не считается доказанным только визуальным diff: для каждого мутанта нужен
исполняемый тест с заранее определённой причиной падения.
## 12. Риски и защита от регрессии
1. **Повторная инверсия не того вектора.** Защита: отдельно тестировать
`toSun`, выбранное окно и `ray.dir` внутрь комнаты.
2. **Тесты снова проходят только при north=0.** Защита: обязательный
`north_deg=90` и некардинальный wrap в unit, smoke и visual fixture.
3. **Незаметное изменение four-phase фона.** Защита: фон резолвится без
compass dependency и получает отдельный regression assertion.
4. **Сломанная настройка у пользователей обходного решения.** Защита:
никаких эвристик/миграции; явное предупреждение в обоих changelog и guide.
5. **Ослабление старых физических ограничений.** Защита: существующие тесты
interior windows, elevation, grazing angle, clipping, wall depth и
obstacles остаются обязательными.
## 13. Rollback
Rollback — единый revert формулы, тестов, golden fixture и документации. Данных
для отката нет: schema/storage не меняются. Частичный rollback недопустим —
нельзя оставить старую формулу с новыми тестами/документацией либо наоборот.
## 14. Release-артефакты
Реализация является user-visible и обязана в одном продуктовом коммите
содержать:
- запись в `docs/CHANGELOG.md`;
- запись в `docs/CHANGELOG.ru.md`;
- исправление `docs/SUN.md`;
- синхронное уточнение `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`;
- обновлённые unit и `demo/smoke_sun.mjs`;
- ненулевую compass-сцену в golden fixture/manifest и принятый diff;
- пересобранные поставляемые bundle-копии по release runbook;
- terminal trailers `Issue: #166` и `User-Visible: yes`.
## 15. Принятые предположения
1. `north_deg` всегда означает показанное стрелкой направление истинного
севера на холсте, а не угол, которым нужно повернуть сам план обратно к
северу.
2. Азимут Home Assistant интерпретируется текущим контрактом: 0° — север,
90° — восток, рост по часовой стрелке.
3. Исправление не мигрирует намеренно неверные обходные значения, потому что
их невозможно надёжно отличить от корректных.
4. Отдельного продуктового решения по UX не требуется: пользователь уже дал
реальный север, меняется только неверный результат этой настройки.