Files
houseplan-card/legacy/specs/166-sun-north-rotation.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00

24 KiB
Raw Permalink Blame History

Issue #166 — солнечные лучи зеркально учитывают направление севера

Статус: ТЗ принято, готово к реализации Дата: 2026-08-16
Тип: bug · приоритет: P1 · user value: 8/10 · complexity: 4/10 · risk: 6/10

Issue: #166
Ветка: issue/166-sun-north-rotation
Канонические документы: SCOPE, SUN, USER-GUIDE, USER-GUIDE.ru, TOUCH-SUPPORT, TESTING, CONFIG-COMPATIBILITY.

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 — направление к Солнцу на холсте.

Каноническая формула:

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 не требуется: пользователь уже дал реальный север, меняется только неверный результат этой настройки.