docs: close M1, M2 and M3 from the spec review of #229

M1: "merge across the whole space when a chain ends" quietly overreached the
owner's split — drawing fixes its own seam, the optimiser fixes what has piled
up, and only the latter comes with a report and an undo. Section 8.6 now scopes
it to the connected component the new chain belongs to.

M2: the tolerance for "something else meets here" was only defined for a
partition-to-partition joint. Room edges, columns and drafts now use the same
EPS_JOIN — a gap cannot be a junction in one case and not in another.

M3: an opening also carries a materialised x/y/angle projection that
CONFIG-COMPATIBILITY (#132) requires to stay in step with its host, and the
code already re-materialises it after every host change. Merging is such a
change; AC3 now fails if only host.t is recomputed and the projection goes
stale.

Issue: #229
User-Visible: no
This commit is contained in:
Codex
2026-08-21 11:46:36 +03:00
parent 5b8b6899da
commit ecc3d6a88b
+62 -15
View File
@@ -31,7 +31,7 @@
## 3. Подтверждённая причина
`_finishWallChain` (`houseplan-card.ts:6558`) режет цепочку по числу поставленных
`_finishWallChain` (`houseplan-card.ts:6538`) режет цепочку по числу поставленных
точек и добавляет **по одной независимой перегородке на сегмент**:
```ts
@@ -72,9 +72,9 @@ run of one thickness», оптимизатор её вызывает (`plan-opti
## 6. Scope
- Чистый модуль слияния (`src/wall-merge.ts`) — правила без DOM, проверяемые юнитами.
- Вызов при завершении цепочки (`_finishWallChain`).
- Вызов при завершении цепочки (`_finishWallChain`, `houseplan-card.ts:6538`).
- Вызов в «Оптимизировать планы» с новым счётчиком в отчёте.
- Пересчёт `host.t` проёмов, висящих на сращиваемых перегородках.
- Пересчёт `host.t` и материализованной проекции `x/y/angle` проёмов, висящих на сращиваемых перегородках.
- i18n строки счётчика (en + ru), оба changelog.
## 7. Не входит в задачу
@@ -112,6 +112,13 @@ run of one thickness», оптимизатор её вызывает (`plan-opti
независимы и пересекаться могут без общей вершины — такой случай не является
стыком и в слиянии не участвует.
**Все четыре причины проверяются одним допуском `EPS_JOIN`** — тем же, которым
проверяется совпадение концов самих перегородок (§8.3). Отдельного допуска для
ребра комнаты, колонны или черновика нет: иначе один и тот же зазор считался бы
стыком в одном случае и не считался в другом, а AC2 перестал бы быть однозначным.
Точка стыка берётся у ребра комнаты — ближайшая вершина полигона, у колонны —
её центр, у черновика — конец сохранённого контура.
### 8.3. Допуски
Один источник истины на обе проверки, объявленный константами в модуле:
@@ -138,18 +145,49 @@ run of one thickness», оптимизатор её вызывает (`plan-opti
2. пересчитывается в долю новой длины с учётом того, какой конец стал началом;
3. если хозяин поглощён — `host.id` переписывается на выжившую запись.
4. **пересчитывается материализованная проекция** `x/y/angle` того же проёма.
`docs/CONFIG-COMPATIBILITY.md` (#132) объявляет legacy `x/y/angle` обязательной
компаньонкой хоста: «the legacy `x/y/angle` siblings remain a materialized
compatibility projection for older readers», и прямо требует, чтобы full export,
plan-only export, merge и оптимизация сохраняли согласованность. В коде это уже
норма: после любого изменения хозяина вызывается `materializePartitionOpening`
(`houseplan-card.ts:7824` — перетаскивание перегородки, `:12007` — правка
проёма). Слияние — такое же изменение хозяина и обязано делать то же самое.
**Инвариант:** координаты центра проёма в единицах плана до и после слияния
совпадают с точностью до `EPS_JOIN`. Именно это проверяет AC3 — не «поле
пересчитано», а «дверь физически не сдвинулась».
совпадают с точностью до `EPS_JOIN` — **и в резолвленном виде, и в
материализованной проекции**. Именно это проверяет AC3: не «поле пересчитано», а
«дверь физически не сдвинулась», причём для обоих читателей — текущего фронтенда
и того, который умеет читать только `x/y/angle`.
### 8.5. Границы применения
- при завершении цепочки сращиваются только записи **этого пространства**;
- слияние применяется многократно до стабилизации: три отрезка подряд дают одну
запись, а не две;
- порядок обхода не влияет на результат (проверяется перестановочным тестом,
как в #218).
### 8.6. Что именно сращивается при завершении цепочки
Решения §4.1 и §4.2 делят работу: рисование чинит **свой** шов, накопленное
чинит «Оптимизировать планы» — там для этого есть отчёт и отмена.
Поэтому при завершении цепочки слияние затрагивает только записи, связанные с
этой цепочкой: сегменты самой цепочки и те существующие перегородки, с которыми
она имеет общий конец, — далее транзитивно, пока цепочка стыков не оборвётся.
Иными словами, компонента связности по общим концам, содержащая хотя бы один
новый сегмент.
Перегородки, не связанные с новой цепочкой, не трогаются, даже если между собой
они образуют шов, подлежащий слиянию. Такой шов дождётся оптимизатора.
**Почему не «всё пространство».** В пространстве владельца из #228 уже лежит
коллинеарная пара `#0`/`#4`. Если дорисовать стену в другом углу, «слияние по
всему пространству» молча починило бы и эту пару — то есть правка старых данных
без отчёта и без выделенной отмены, ровно то, что §4.2 обещает делать только по
явной команде.
## 9. Данные, i18n, a11y, privacy, security
- **Данные:** формат перегородки не меняется; меняется их количество и `host.id`
@@ -182,10 +220,13 @@ run of one thickness», оптимизатор её вызывает (`plan-opti
2. **AC2 — узел с причиной остаётся.** Слияния не происходит, если на общем конце
есть третья перегородка, ребро комнаты, колонна или конец черновика — четыре
отдельных случая. **Доказательство:** `unit`.
3. **AC3 — проём не двигается.** Перегородка с дверью посередине сращивается с
соседней; координаты центра проёма в единицах плана до и после совпадают,
`host.id` указывает на существующую запись, `host.t` в пределах `[0,1]`.
Тест красный до реализации §8.4. **Доказательство:** `unit`.
3. **AC3 — проём не двигается, в обоих представлениях.** Перегородка с дверью
посередине сращивается с соседней; координаты центра, полученные через
`resolvePartitionOpening`, до и после совпадают, `host.id` указывает на
существующую запись, `host.t` в пределах `[0,1]`. Одновременно материализованные
`x/y/angle` того же проёма пересчитаны и согласованы с резолвленной позицией
(#132). Тест красный до реализации §8.4 — и остаётся красным, если пересчитан
только `host.t`, а проекция устарела. **Доказательство:** `unit`.
4. **AC4 — разная толщина не сращивается.** Два коллинеарных отрезка с разными
`cm` остаются двумя записями. **Доказательство:** `unit`.
5. **AC5 — допуски.** Отрезки, разведённые на расстояние больше `EPS_JOIN`, не
@@ -198,8 +239,10 @@ run of one thickness», оптимизатор её вызывает (`plan-opti
отчёт показывает их число, повторный запуск ничего не меняет
(идемпотентность). **Доказательство:** `unit` (`optimizePlans`).
8. **AC8 — ничего лишнего.** Стены комнат, колонны и черновики контуров не
изменяются; количество комнат и их геометрия те же.
**Доказательство:** `unit` + diff review.
изменяются; количество комнат и их геометрия те же. Отдельно: завершение
цепочки не трогает перегородки вне её компоненты связности — пространство с
готовым швом в стороне после рисования новой стены сохраняет этот шов до
запуска оптимизатора (§8.6). **Доказательство:** `unit` + diff review.
9. **AC9 — release-артефакты.** Оба changelog, строка счётчика в обоих языках,
`dist`/demo/integration бандлы идентичны друг другу.
**Доказательство:** diff + сверка копий бандла.
@@ -223,6 +266,8 @@ run of one thickness», оптимизатор её вызывает (`plan-opti
| `partition-merge-ignores-thickness` | сращивать при разном `cm` | юнит AC4 |
| `partition-merge-ignores-junction` | сращивать через примыкание третьей стены | юнит AC2 |
| `partition-merge-keeps-relative-t` | не пересчитывать `host.t` | юнит AC3 |
| `partition-merge-skips-materialization` | не обновлять `x/y/angle` проёма | юнит AC3 |
| `chain-merge-sweeps-whole-space` | сращивать все перегородки пространства, а не компоненту цепочки | юнит AC8 |
## 15. Release-артефакты
@@ -248,6 +293,8 @@ run of one thickness», оптимизатор её вызывает (`plan-opti
бы оно было детерминированным и не зависело от времени; это — самое простое.
3. **Конкретные значения `EPS_ANGLE` и `EPS_JOIN`** подбираются при реализации;
ТЗ фиксирует только их природу (доли шага сетки) и границы (§8.3).
4. **Слияние при завершении цепочки применяется ко всему пространству**, а не
только к новым записям: цепочка могла примкнуть к нарисованному ранее отрезку,
и шов на стыке — тот же самый случай.
4. **Область слияния при завершении цепочки** описана нормативом §8.6, а не
здесь: это не свободное предположение, а граница между двумя решениями
владельца (§4.1 и §4.2). Ревизия r2: прежняя редакция говорила «применяется ко
всему пространству» — сращивало бы и старые швы в другом углу, обходя обещанный
для них отчёт и отмену (находка M1 ревью r1).