fix: canonicalize persisted geometry

Issue: #224
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-22 14:47:38 +03:00
parent 8442538b6b
commit 4a798e3e13
25 changed files with 1489 additions and 196 deletions
+23
View File
@@ -45,6 +45,29 @@ centimetres, screen-fixed strokes/handles, plan-relative icon sizes and the
grid pitch must not receive that factor again. Full, static and hidden
isometric renderers share this classification.
### Persisted coordinate canonicalisation
Every current config/layout write removes IEEE-754 representation tails from
persisted geometry by rounding an explicit allow-list to nine decimal places.
This is not grid snapping: an off-grid or diagonal coordinate stays where the
editor put it, with a maximum normalized change of `5e-10`. The contract is
mirrored in Python and TypeScript and normalizes negative zero.
The allow-list covers room outlines/extents, exact wall endpoints, openings
(including angle, length and hosted `t`), decor transforms, drafts,
partitions, columns, open spans, backdrop transforms, marker angle and layout
`x/y`. It deliberately excludes `cell_cm`, `plan_aspect`, `view_box`,
physical centimetre fields, colours/opacities/live values, presentation scales
and vacuum affine calibration. Unknown/future numeric fields round-trip
unchanged.
Schema validation is the public door; the common config/layout storage helpers
repeat the same idempotent operation for internal import, maintenance and
startup-recovery writers. A canonical read/write echo is a no-op: optimistic
locking is still checked, but the revision, update event and maintenance Undo
snapshot do not move. Existing stores are not rewritten on read; Optimize Plans
remains the explicit bulk-cleanup path.
## Model
| Concept | Before | Now |
+9
View File
@@ -2,6 +2,15 @@
## Unreleased
- Every config and device-layout write now canonicalizes persisted geometry to
nine decimal places without snapping it to the grid. This removes invisible
floating-point tails before they can break shared walls, room unions or Glow;
import, maintenance recovery and server Undo use the same invariant. Repeating
an unchanged canonical Save no longer creates a revision or consumes the
one-deep Undo snapshot. Existing noisy plans are cleaned on their next write,
or immediately through Optimize Plans
([#224](https://github.com/Matysh/houseplan-card/issues/224)).
- Grid precision no longer changes the appearance of the same physical plan.
Room and wall outlines, openings and their hit areas, Plan hints, the static
card and hidden isometric geometry now retain the `cell_cm: 5` visual size at
+10
View File
@@ -8,6 +8,16 @@
## Не выпущено
- Каждая запись конфигурации и раскладки устройств теперь приводит геометрию к
девяти десятичным знакам, не привязывая её к сетке. Невидимые
floating-point-хвосты удаляются до того, как смогут сломать общие стены,
объединение комнат или Glow; тот же инвариант действует для импорта,
восстановления служебных операций и серверной отмены. Повторное сохранение
уже канонических данных больше не создаёт ревизию и не съедает одношаговую
отмену. Старый план очищается при следующей записи либо сразу через
«Оптимизировать планы»
([#224](https://github.com/Matysh/houseplan-card/issues/224)).
- Точность сетки больше не меняет внешний вид одного и того же физического
плана. Контуры комнат и стен, проёмы и их зоны попадания, подсказки редактора,
статическая карточка и скрытая изометрия сохраняют эталонный вид
+21
View File
@@ -48,6 +48,27 @@ Unknown future fields remain outside this report and continue to follow the
backend's forward-compatibility policy. Absence from the report is therefore
not permission to delete a field.
## Canonical geometry on write (#224)
Config and layout schemas canonicalize only named geometry numbers to nine
decimal places. The common storage helpers repeat the same idempotent contract
for internal writers, while the frontend adopts the exact candidate it sends.
This prevents ULP noise without changing the schema, JSON number type,
model/store version or any user-visible placement.
The operation is lossless at the product scale and intentionally narrow.
`view_box`, `cell_cm`, `plan_aspect`, physical centimetre values,
presentation settings, colours, opacity/brightness/temperature, vacuum
calibration and unknown/future numeric fields retain their exact input values.
No recursive “round every number” migration is allowed.
Existing stores remain byte-for-byte untouched on read. Their geometry becomes
canonical on the next config/layout write; Optimize Plans is the immediate bulk
path. Optimize/Import/repair Undo restores the previous semantic geometry and
unknown fields in canonical representation, not the invisible noisy IEEE-754
tail. A repeated canonical Save with the current revision is a no-op and does
not invalidate the one-deep maintenance backup.
## Open-passage opening type (#157)
`space.openings[].type` additionally accepts the literal `passage`. Its
+1 -1
View File
@@ -22,7 +22,7 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
| Item | State |
|---|---|
| Version | **v1.66.0** everywhere (manifest, const.py, package.json, CARD_VERSION) — stable promotion of the published v1.66.0-beta.1 product line |
| Current local cycle | Post-v1.66.0 Unreleased development includes reliable Plan drawing (#228): exact Shift rays, ambiguous-node blocking, active axis/node chrome, room creation from an existing face with an explicit ≤2 cm repair, and room deletion with Keep/Delete walls consequences. Entity/parent marker deduplication (#226) and exact coordinate maintenance (#223) are also included. |
| Current local cycle | Post-v1.66.0 Unreleased development includes reliable Plan drawing (#228): exact Shift rays, ambiguous-node blocking, active axis/node chrome, room creation from an existing face with an explicit ≤2 cm repair, and room deletion with Keep/Delete walls consequences. Entity/parent marker deduplication (#226), explicit coordinate maintenance (#223) and canonical geometry on every write (#224) are also included. |
| Hidden Labs Stage | #89 Stage 1 ships in v1.63.0-beta.1. #122 Stage 2 ships in v1.64.0 and evolves the same hidden, expiring `iso` experiment with matte walls, a low exterior floor edge, restrained shared shadows and live vertical door/window/gate panels. Flat remains default; editors and `houseplan-space-card` remain flat; live floor effects and HA actions remain unchanged. Public activation remains a separate task. |
| Workflow | Superseded 2026-08-12: the pre-1.62 rule of "local edits without tests or commits" is **dead** — since release 1.62 every product change follows `PROCESS.md` (issue in `S5-ready`+, branch `issue/<NN>-slug`, trailers on every commit, review pipeline; `AGENTS.md` is the summary). Release mechanics below remain current. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Release bodies are short and bilingual (Russian first): only significant user changes get individual bullets, while minor/code-only work is grouped as `Мелкие исправления и улучшения` / `Small fixes and improvements`; every body ends with separate links to the Russian and English changelogs. Detailed RU/EN changelog bullets may link the corresponding closed GitHub Issues; open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- <tag> --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand |
| GitHub | https://github.com/Matysh/houseplan-card — [Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical task records; their labels carry priority and workflow status (`PROCESS.md` §9). GitHub Projects is no longer used. `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
+14 -1
View File
@@ -2090,11 +2090,24 @@ require hands on real hardware — they remain for the human pass.
`moved: 0` and a positive cleaned-coordinate count. Preview distinguishes
updated spaces from removed coordinate noise; Cancel writes nothing;
Apply stores exact grid nodes in one transaction; the next run is a no-op
and server Undo restores the original noisy bits. A rejected hosted
and server Undo restores the original geometry in canonical
representation. A rejected hosted
partition contributes nothing to the counter
[unit: align-grid + plan-optimizer + i18n; auto:
smoke_optimize_coordinate_canonicalization; mutation:
`snapn-returns-input-near-node`].
- [ ] **Every write prevents new ULP coordinate noise (#224)**: config/layout
schema, import, direct storage writers, startup recovery and maintenance
Undo produce the same nine-decimal allow-listed geometry as the frontend.
A first noisy write creates one canonical revision; a repeated canonical
write creates no store write/event/revision and preserves the maintenance
backup. `view_box`, physical/presentation values, colours and vacuum
calibration remain exact; the six-room #218 union and Glow clip stay
non-empty [unit: coordinate-canonicalization + physical-geometry;
backend: test_coordinate_canonicalization + test_ha_websocket +
test_ha_import_export; mutations: `schema-quantization-removed`,
`frontend-writes-raw-coords`, `quantization-hits-allowlist`,
`import-path-bypasses-schema`; pre-release: golden verify].
- [ ] Optimizer migration safety: legacy decor width/text size is clamped to
the backend schema, `fill: true` receives explicit fill style, invalid
legacy `plan_scale` is preserved for repair, an already canonical plan is
+12 -1
View File
@@ -1332,6 +1332,17 @@ show_signal: true
## 19. Обслуживание планов
Обычное сохранение конфигурации или позиции устройства автоматически убирает
из координат невидимый вычислительный шум. Это **не привязка к сетке**:
диагональные стены, свободный декор и другие допустимые положения остаются на
своих местах. Импорт и серверная отмена используют то же правило. Повторное
сохранение уже неизменного плана не создаёт новую ревизию и не отменяет
доступность последней служебной отмены.
Существующий план не переписывается при простом открытии. Его геометрия
очистится при следующем изменении; если очистить всё нужно сразу, используйте
явную оптимизацию ниже.
Кнопка **Общие настройки → Оптимизировать планы** запускает явное обслуживание. Сначала показывается точный предпросмотр; данные не меняются до подтверждения.
### Что делает оптимизация
@@ -1365,7 +1376,7 @@ show_signal: true
### Риск и отмена
Старые и импортированные объекты между узлами будут привязаны к сетке. Новые редакторы такие координаты не создают. После оптимизации доступна одна серверная отмена, но только до следующего изменения плана. Новый edit делает резервную копию оптимизации устаревшей.
Старые и импортированные объекты между узлами будут привязаны к сетке. Новые редакторы такие координаты не создают. После оптимизации доступна одна серверная отмена, но только до следующего изменения плана. Отмена возвращает прежнюю геометрию и неизвестные поля в чистом числовом представлении, не восстанавливая невидимый floating-point шум. Новый edit делает резервную копию оптимизации устаревшей.
Обычное открытие и редактирование плана не удаляет даже очень короткие точные
границы толщины. Описанное схлопывание выполняется только после явного