Files
houseplan-card/docs/specs/150-wall-thickness-transition.md
T
2026-08-18 17:36:54 +03:00

29 KiB
Raw Blame History

Issue #150 — Точная геометрия коллинеарного перепада толщины стен

  • Issue: https://github.com/Matysh/houseplan-card/issues/150
  • Редакция: r2 после SPEC-REVIEW-150-r1; статус задачи определяется только метками issue
  • Тип / приоритет: bug / P2
  • Оценка: пользовательская ценность 8/10; ценность для разработки 8/10; сложность 7/10, риск 8/10
  • Область: atomic wall intervals, внешний фасад, wall body/paper/clean floor, Plan, View/киоск, static renderer, hidden Iso, Glow и солнце
  • Модель данных: без изменений и миграции
  • Связано: docs/WALL-THICKNESS.md, docs/ARCHITECTURE.md, docs/CANVAS.md, docs/ISOMETRIC.md; #123 и #141

1. Сценарий и продуктовый контекст

Персона: администратор дома, исправляющий архитектуру плана в desktop Plan Editor.

Поверхность и момент: после Split исходной комнаты с нулевыми стенами пользователь выбирает внешнюю стену одной дочерней комнаты, задаёт 10 см и нажимает «Применить ко всем стенам комнаты».

До → после, без терминов реализации: сейчас после Split толщина выбранной комнаты сохраняется правильно, но у точки разделителя не заканчивается и визуально продолжается по внешним стенам соседней комнаты, для которых задано 0. После исправления выбранные участки имеют полные 10 см до точки разделителя, соседняя комната остаётся без толщины, а граница видна как точная ступень.

Первоначальное описание «примерно 5 вместо 10» уточнено после ревью. На скриншоте внутренняя половина внешней стены действительно занимает 5 см, потому что действующий контракт стены — centreline ±½; внешняя половина лежит на белом поле и читается по штриховке. Полная глубина выбранного участка равна 10 см. Реальный дефект скриншота — отсутствие границы 10 → 0: штриховка 10-сантиметрового body продолжается снаружи вдоль нулевого фасада соседа.

Задача поддерживает J4 и J6 из docs/SCOPE.md: штатное редактирование должно создавать измеримо правдивую и стабильно читаемую архитектуру.

2. Красное воспроизведение на dev и подтверждённая причина

На конфигурационном уровне команда уже работает правильно:

  • все допустимые atomic intervals выбранной комнаты получают 10 см;
  • общий разделитель получает 10 см как одна физическая стена;
  • внешние стены соседней комнаты остаются 0.

Повторная проверка после SPEC-REVIEW-150-r1 выполнена на текущем dev a05aa5d двумя путями: через фактические обработчики Plan Editor _splitClick() → _wallThickClick() → _wallThickApply(true) и через исполняемые setWallThicknessForRoom() → wallIntervals() → wallBodiesGeometry() с production-параметром coordScale = 1000.

Красный fixture: прямоугольник [100,100]–[900,900], вертикальный Split в x = 500, слева 10 см, справа 0, разделитель 10 см. Данные дают:

  • левый верхний interval: cm = 10;
  • правый верхний interval: cm = 0;
  • divider: cm = 10;
  • ожидаемая полная глубина: 8.333333 render units.

Но текущий masonry path содержит верхнюю грань M 95.833… 95.833… L 900 95.833…: глубина 10 см протянута через весь правый нулевой interval до x = 900. Поэтому probe на правом участке, например [700, 96], ошибочно находится внутри masonry, хотя обязан быть вне его. Сечение выбранного левого участка при этом полное — 8.333333 units; тест, проверяющий только толстую сторону, дефект не обнаруживает.

Подтверждённая причина находится в exteriorBoundaryProfile(). Коэффициент t точки разбиения нормирован на диапазон 0…1, но сравнивается с eps в render units. При обычных pitch = 1/240 и coordScale = 1000 используемый eps равен примерно 0.6667; проверки t > eps && t < 1 - eps становятся невыполнимыми, а дедупликация t тем же допуском также размерностно неверна. Breakpoint x = 500 после boolean union не материализуется, и весь коллинеарный edge получает depth того interval, на который попал его midpoint. Воспроизведение обязано использовать production scale; проверка с coordScale = 1 не ловит ошибку.

Это не дефект сохранения команды «Применить ко всем», не изменение семантики centreline ±½ и не исправляется дублированием wall key, CSS stroke или визуальной накладкой только в Plan.

3. Решения владельца

Владелец 15.08.2026 принял defaults Q1–Q4. Каноническая запись: https://github.com/Matysh/houseplan-card/issues/150#issuecomment-5301107134

  1. Выбранная комната получает полные 10 см на внешних участках вплоть до разделителя; общий разделитель — 10 см; остальные внешние стены соседней комнаты — 0.
  2. На общем внешнем фасаде переход точный и ступенчатый на endpoint разделителя, без сужения, растягивания на соседа или taper.
  3. Инвариант действует для любых соседних коллинеарных интервалов разной толщины (10 → 0, 10 → 20 и т. п.).
  4. Исправляется общая каноническая геометрия Plan, View/киоска, static, hidden Iso/floor footprint и препятствий Glow/солнца; существующие конфиги исправляются без миграции.

4. Скоуп

В задачу входят:

  1. все пары соседних collinear exterior atomic intervals с неравной допустимой толщиной, включая один нулевой;
  2. точная граница перепада в сохранённом atomic endpoint;
  3. сценарий endpoint разделителя после Split и аналогичная топология, независимо от направления, winding, room id и порядка комнат;
  4. полная толщина каждого участка, корректные inward/outward faces и clean floor по обе стороны перехода;
  5. корректное соединение с общей стеной/разделителем, заканчивающейся в той же точке;
  6. единая masonry/paper geometry для всех render/light consumers;
  7. уже сохранённые планы без config rewrite;
  8. unit, browser smoke, visual golden, документация и RU/EN changelog.

5. Не входит в задачу

  • изменение UX или семантики «Применить ко всем стенам комнаты»;
  • изменение выбора atomic interval, диапазона толщины или wall key format;
  • автоматическое выравнивание толщины соседних комнат;
  • плавный bevel/taper между коллинеарными участками;
  • новая пользовательская настройка вида перехода;
  • изменение Split, Merge, Resize или Boundary как редакторских операций;
  • исправление произвольных near-miss, X-crossing или независимых junctions, которые относятся к #141;
  • изменение фасадного инварианта corner Split из #123;
  • миграция, schema version, backend, import/export или materialisation на read;
  • отдельная wall model для Iso или света;
  • изменение opening semantics, кроме обязательного отсутствия регрессии на opening, расположенном рядом с перепадом.

6. Каноническая математическая семантика

6.1. Atomic intervals

Максимальная collinear boundary разбивается во всех структурных точках:

  • endpoints исходных room edges и дочерних edges после Split;
  • endpoints точных WallEntry.a/b и совместимых legacy breaks;
  • начало/конец open_spans и physical opening coverage, когда они создают действующую атомарную границу;
  • junctions и точки изменения effective thickness.

Каждый получившийся solid interval имеет постоянную effective thickness cm_i и half-depth

h_i = wallCmToUnits(cm_i, cell_cm, grid_pitch) / 2

Zero interval (cm_i = 0) остаётся границей пола, но не создаёт masonry body. Ни порядок комнат, ни midpoint compacted key не могут удалить точный breakpoint.

6.2. Полная толщина участка

Для любой точки внутри solid interval, расположенной дальше geometry epsilon от его endpoints и openings, сечение wall body по нормали к centreline обязано иметь:

  • полную глубину 2 × h_i, соответствующую cm_i;
  • внутреннюю грань на h_i от centreline;
  • внешнюю грань на h_i от centreline;
  • отсутствие удвоения или потери alpha/hatch из-за перекрывающихся room rings.

Допуск используется только для численной устойчивости и не может объяснять видимое сужение 10 см до 5 см.

6.3. Жёсткая ступень h1 → h2

Пусть два соседних collinear intervals встречаются в точке P, имеют half-depth h1 и h2, h1 != h2.

  1. До P обе faces находятся ровно на ±h1; после P — на ±h2.
  2. Разность глубин закрывается в P сегментами, перпендикулярными centreline.
  3. На ненулевой длине вокруг P нет линейной интерполяции, mitre вдоль прямой, taper, усреднения, overshoot или распространения большего h на соседа.
  4. Breakpoint в результирующей geometry совпадает с P в пределах общего geometry epsilon.
  5. Правило симметрично для 0 → h, h → 0, h1 → h2, обратного направления boundary и любого winding.
  6. Boolean cleanup может объединить совпадающие рёбра, но не имеет права удалить поперечные faces ступени или сдвинуть их от P.

6.4. Endpoint разделителя

Если в P к фасаду примыкает shared wall после Split:

  • shared wall сохраняет полную назначенную толщину и одну physical identity;
  • её тело заканчивается/соединяется с внутренней частью фасада без щели;
  • она не выступает наружу и не изменяет exterior face фасада, как требует контракт #123;
  • внешний interval выбранной комнаты сохраняет h_selected до P;
  • соседний внешний interval начинает собственный h_neighbor непосредственно после P;
  • соединение не оставляет triangular notch, half-depth strip, двойной hatch или ложный passage для света.

Для сценария issue это означает 10 см exterior → P → 0 см exterior, при этом shared divider в P имеет 10 см и остаётся внутри facade envelope.

6.5. Clean floor и paper

  • paper footprint растёт наружу по локальному h_i и повторяет ту же ступень;
  • clean floor каждой комнаты отступает внутрь по её effective local half-depth;
  • выбранная комната не получает клин пола между 10-сантиметровой стеной и разделителем;
  • соседняя комната с 0 сохраняет floor до centreline своего внешнего участка;
  • displayed area вычисляется из исправленного clean floor без изменения centreline room polygon или stored area data;
  • courtyard/nested geometry сохраняет действующий evenodd/topology contract.

7. Архитектурный контракт реализации

Конкретный helper и boolean primitive выбирает автор, но обязательны границы:

  1. wallIntervals()/эквивалент остаётся каноническим источником interval endpoints и effective cm.
  2. Exterior profile обязан материализовать каждый thickness breakpoint до offset/union и сохранить две независимые face depths в этой точке. Обычный polygon vertex с одним offset, не способный выразить discontinuity, не является достаточным представлением.
  3. Допустимые решения — interval slabs/quads с точными butt faces, профиль с duplicated transition vertices/explicit step faces или эквивалентная детерминированная конструкция. Рисование corrected CSS stroke поверх ошибочного body запрещено.
  4. Настоящие non-collinear фасадные углы продолжают использовать действующий bounded mitre/bevel (MITRE_LIMIT); collinear thickness step не является углом для mitre.
  5. Shared/internal bodies сначала ограничиваются interior side exterior envelope, затем объединяются с точным shell — инвариант #123 не ослабляется.
  6. Один результат wallBodiesGeometry()/эквивалента порождает masonry path, paper path, clean-floor relation и light/sun barriers. Static и full card не имеют отдельных корректирующих веток.
  7. Iso потребляет тот же canonical masonry/floor footprint и экструдирует уже правильный stepped outline; самостоятельное сглаживание ступени запрещено.
  8. Порядок комнат, stable id rename, winding и направление интервала не меняют геометрическое множество результата.
  9. Новая топология входит в существующий structural fingerprint/cache. HA state tick, hover и activity effects не запускают boolean rebuild.
  10. При boolean failure действует существующий fail-closed contract: нельзя воскресить raw per-room rings, известные half-depth/facade дефектом.

8. Openings, виртуальные границы и junction regression contract

Задача не меняет типы проёмов, но исправленная geometry обязана сохранять:

  • opening association с фактическим atomic interval;
  • full-depth cut и jamb по локальной толщине каждой покрытой части;
  • при crossing thickness transition — точный stepped tunnel без cut соседнего interval и без neutral-paper leak;
  • window/door/gate light semantics из docs/WALL-THICKNESS.md;
  • virtual open_span как реальный break, а не мост для переноса большей толщины;
  • примыкания независимых partitions/drafts/columns по контракту #141;
  • corner Split exterior envelope по контракту #123.

Если opening не может быть однозначно связан после исправления, действует существующий fail-dark/fail-closed путь; новая nearest-wall эвристика не вводится.

9. Поверхности-потребители

Одинаковое геометрическое множество требуется для:

  1. Plan Editor, включая hover/selection Thickness поверх фактического body;
  2. View и kiosk full renderer;
  3. houseplan-space-card/static renderer;
  4. hidden Iso wall volume, top face и floor slab footprint;
  5. room fill и clean-floor clipping;
  6. displayed clean area;
  7. Glow/spill occluders и placement checks;
  8. sun barriers/wedges;
  9. paper/content frame, когда внешняя половина стены расширяет план.

Разница только из-за presentation policy (например, hatch suppression при малой экранной толщине) допустима; physical outline и проходы должны совпадать.

10. Модель данных, compatibility и миграция

Формат остаётся прежним:

interface WallEntry {
  key: string;
  cm: number;
  a?: number[];
  b?: number[];
}
  • новые поля, aliases и schema version не добавляются;
  • точные a/b и legacy midpoint-only keys читаются по действующему contract;
  • normalisation/materialisation выполняются только при явном редактировании, как сейчас;
  • открытие/рендер старого плана не вызывает save и не переписывает config;
  • backend validation, import/export и wire payload не меняются;
  • rollback возвращает прежний renderer без потери данных, хотя визуальный дефект снова проявится.

11. UX, i18n, accessibility и touch

Controls, dialogs, tool sequence, labels и focus не меняются. Новые locale keys не требуются.

Plan Editor остаётся desktop-first. Touch editor: best effort / intentionally degraded. Safety floor обязателен: тот же сохранённый config на touch не может получить другую физическую геометрию из-за pointer type. View и kiosk fully supported и обязаны показывать тот же полный stepped body.

prefers-reduced-motion не затрагивается. Исправление geometry не добавляет animation и не использует цвет как единственное доказательство толщины.

12. Acceptance criteria

  1. [unit] Production-scale fixture из §2 сохраняет data profile 10 см на внешних стенах выбранной комнаты и shared divider, 0 на внешних стенах соседа; тест явно передаёт coordScale = 1000.
  2. [unit] Нормальное сечение каждого 10-сантиметрового участка до endpoint имеет полные 10 см: 5 см наружу и 5 см внутрь, в численном пересчёте render units.
  3. [unit + golden] На endpoint разделителя фасад меняется 10 → 0 точной поперечной ступенью; probes на нулевой стороне находятся вне masonry, нет taper, переноса 10 см на соседа, зуба или щели.
  4. [unit] Матрица 0 ↔ 10, 10 ↔ 20, 1 ↔ 100 и равных толщин корректна в обоих направлениях, при обратном winding и перестановке/переименовании комнат.
  5. [unit] Shared divider сохраняет полную толщину, соединяется внутри и не расширяет фасад наружу.
  6. [unit] Paper, clean floor и displayed area следуют локальным depths и точной ступени без клина/утечки пола.
  7. [smoke + golden] Plan, View/kiosk, static и hidden Iso имеют одинаковый wall/floor outline; Glow и солнце блокируются ровно нарисованным body.
  8. [unit + smoke] Openings около/через transition используют локальные depths и не создают ложных cuts или двойной fill.
  9. [unit + smoke] Регрессии #123 (corner Split facade) и #141 (independent junctions) остаются закрытыми; nested/courtyard/open-span fixtures не меняются ошибочно.
  10. [unit + ревью кода] Старый config исправляется вычисляемо без save/migration и остаётся совместимым с rollback.
  11. [unit + ревью кода] Structural geometry кешируется; ordinary HA tick не повторяет topology.
  12. [unit + ревью кода] Boolean failure остаётся fail-closed и не воскрешает raw per-room rings.

13. Проверки и доказательства

Unit — данные и точная геометрия

  • красный production-scale fixture из §2: Split rectangle, all walls 0, all-room 10 для одной child room; assert effective intervals, shared wall ownership, полную толстую сторону и отсутствие masonry probes на нулевой стороне;
  • отдельная нормализованная вариация с coordScale = 1 не заменяет fixture с coordScale = 1000;
  • cross-section/point containment на interior участках доказывает exact ±h_i, а не только сравнивает bbox/скриншот;
  • transition faces совпадают с divider endpoint и перпендикулярны centreline;
  • parameter matrix 0/1/10/20/100, оба направления, horizontal/vertical/45°;
  • room order/id/winding invariance через symmetric-difference area;
  • equal neighbouring depths не создают лишнюю seam/step;
  • shared divider: no exterior difference сверх локального exterior profile;
  • paper geometry и clean-floor areas по обе стороны;
  • opening до, после и пересекающий transition;
  • #123 corner Split, #141 virtual/independent junction, open spans, nested/courtyard и boolean failure regressions;
  • full/static/Iso consumers получают один fingerprinted canonical result.

Browser smoke

  1. В Plan повторить пять шагов issue и визуально/инструментом проверить каждую стену выбранной и соседней комнаты.
  2. Переключить Plan → View → kiosk/static fixture → hidden Iso без изменения silhouette/step.
  3. Проверить Undo/Redo all-room thickness: одна команда возвращает прежний профиль, повтор применяет исправленную geometry.
  4. Добавить opening рядом с transition и проверить cut/fill/Glow.
  5. Изменить HA light state: topology fingerprint и masonry path не меняются.

Golden и performance

  • golden исходного issue крупным планом в Plan и View;
  • static и hidden Iso того же fixture;
  • 10 → 20, 45° transition, opening near transition, light/sun state;
  • diff review проверяет именно ступень и полную толщину, а не массовое обновление anti-aliasing;
  • performance evidence подтверждает отсутствие geometry rebuild на HA ticks и отсутствие существенной регрессии canonical pass на representative plan.

Golden, smoke и performance запускаются в release gate перед бетой; цикл реализации — typecheck, unit, build по процессу. Полный HA harness каноничен в Linux CI.

14. Release-артефакты

В том же user-visible коммите реализации обязательны:

  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md;
  • docs/WALL-THICKNESS.md — явный контракт collinear discontinuity;
  • docs/ARCHITECTURE.md — canonical interval/step representation и consumers;
  • при необходимости docs/ISOMETRIC.md и docs/CANVAS.md, если комментарии о footprint требуют уточнения;
  • docs/USER-GUIDE.ru.md — только если там описан отличавшийся результат all-room thickness; новых controls нет;
  • unit fixture, smoke scenario и принятые golden/baselines;
  • docs/TESTING.md, если меняется или добавляется release scenario.

Новых i18n, backend/security artifacts не требуется. Если реализация потребует формат данных или отдельную пользовательскую настройку, задача возвращается владельцу до кода.

15. Риски и rollback

Основные риски: новый внешний зуб вместо half-depth, потеря floor hole, расхождение full/static/Iso, ошибочный opening cut и рост boolean cost. Они закрываются symmetric-difference, cross-section, consumer parity и performance проверками раздела 13.

Rollback — возврат geometry-кода и документации без миграции. Сохранённые walls не изменяются и остаются читаемыми обеими версиями.

16. Принятые технические предположения

  1. Breakpoint уже доступен из точных atomic intervals; исправлению не нужен новый persisted vertex или WallEntry field.
  2. Ступень существует на обеих faces wall body. При h2 = 0 обе faces второго участка сходятся на centreline ровно в P, а не заранее.
  3. Поперечная face в transition является частью physical boundary, но не новым самостоятельным wall object и не target инструмента Thickness.
  4. Opening, пересекающий transition, остаётся допустимым и режет union реальных локальных depths; запрет такого opening не является исправлением.
  5. Existing structural cache key на _cfgEpoch/complete geometry расширяется только если текущий fingerprint не содержит всех endpoints/thicknesses.