mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
353 lines
24 KiB
Markdown
353 lines
24 KiB
Markdown
# 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 не требуется: пользователь уже дал
|
||
реальный север, меняется только неверный результат этой настройки.
|