diff --git a/docs/specs/239-grid-scale-invariance.md b/docs/specs/239-grid-scale-invariance.md new file mode 100644 index 00000000..d52d8396 --- /dev/null +++ b/docs/specs/239-grid-scale-invariance.md @@ -0,0 +1,480 @@ +# Issue #239 — масштаб сетки не меняет внешний вид плана + +- Дата: 2026-08-22 +- Тип: bug · приоритет P1 · пользовательская ценность 9/10 +- Сложность 8/10 · риск 8/10 · обычный трек +- Issue: [#239](https://github.com/Matysh/houseplan-card/issues/239) +- Ветка: `issue/239-grid-scale-invariance` + +Канонические документы: `docs/SCOPE.md`, `docs/CANVAS.md`, +`docs/WALL-THICKNESS.md`, `docs/TOUCH-SUPPORT.md`, +`docs/CONFIG-COMPATIBILITY.md`, `docs/USER-GUIDE.ru.md`. + +Связанная реализованная задача: [#230](https://github.com/Matysh/houseplan-card/issues/230) +зафиксировала физический шаг штриховки стен и является подмножеством этого +контракта. + +## 1. Сценарий и персона + +Администратор дома создаёт новый этаж в desktop-редакторе и выбирает точность +сетки. Для подробного плана квартиры ему нужен шаг 1 см, а для старого плана +может оставаться 5 см. В имперской системе тот же новый этаж начинается с шага +1 дюйм. + +При одинаковых физических размерах комнат, стен, проёмов и объектов человек +должен видеть один и тот же план. Выбор более мелкой сетки даёт больше доступных +координат для точного редактирования, но не превращает створки дверей, контуры, +подсказки или изометрические стены в более тонкие и мелкие элементы. + +Задача поддерживает J4 и J6 из `docs/SCOPE.md`: точный план без внешнего CAD и +предсказуемое обслуживание существующей схемы. + +## 2. Что человек увидит до и после + +**До:** на физически одинаковом плане при `cell_cm: 1` часть элементов выглядит +в несколько раз тоньше или меньше, чем при `cell_cm: 5`. Особенно заметны +створки и дуги проёмов, контуры комнат и часть линий/узлов редактора. Новое +пространство всегда предлагает 5 см на клетку, а в имперском интерфейсе поле +масштаба всё равно выражено в сантиметрах. + +**После:** физически одинаковые планы при 1 и 5 см на клетку выглядят одинаково +во View, Plan, Devices, Background, static/embedded card и скрытой изометрии. +Различаются только плотность сетки и точность snap. Новое пространство начинает +с 1 см на клетку в metric HA и с 1 дюйма на клетку в imperial HA. Старые +пространства и сохранённые значения не переписываются. + +## 3. Подтверждённый диагноз + +### 3.1 Что уже масштабируется правильно + +- Толщина стен, перегородок и колонн переводится из cm через + `wallCmToUnits()`. +- Размеры decor и мебели проходят через cm↔units helpers. +- Glow хранит радиус в физических единицах. +- #230 сделал шаг и толщину штриховки физическими. +- Узлы привязки #137 имеют физический радиус 5/10 см. +- Device markers и room labels получают размер через `iconUnit()` / `iconCqw()`; + screen-space trail/chrome использует `vector-effect` или расчёт из текущего + `view.w / clientWidth`. + +Эти пути нельзя домножать повторно. + +### 3.2 Где инвариант нарушен + +В старом масштабе `cell_cm: 5` в рендере накопились константы в SVG user units. +На физически эквивалентном плане с `cell_cm: 1` координаты и frame в пять раз +больше, а константы остаются прежними и после одинакового fit становятся в пять +раз мельче на экране. + +Подтверждённые группы: + +1. `src/render/opening-symbol.ts`: толщина створок 3/3.5 units, jamb/glass + strokes 1.5/2.5, fallback depth и padding outline/hit 4–12 units. +2. `src/houseplan-card.ts`: outline/hit rect проёма, offset lock badge, + preview/orphan dots, Plan vertices, alignment dots и отдельные SVG strokes. +3. `src/styles.ts`: room/open-boundary/wall outlines, opening arc/outline, + hover halo, draft/preview/repair lines, dash patterns и часть editor chrome. +4. `src/space-render.ts` / `src/space-card.ts`: статические room/wall/opening + counterparts с теми же сырыми units. +5. Скрытая изометрия: `ISO_WALL_HEIGHT`, `ISO_FLOOR_EDGE_HEIGHT`, shadow offset + и SVG blur заданы в user units. При увеличившемся физически эквивалентном + плане высота и тени относительно фасада уменьшаются. + +### 3.3 Default и compatibility + +- Ручной create и floors-import draft отдельно задают `cellCm: 5`. +- Поле всегда показывает canonical cm и строку `cm per cell`, даже когда + `hass.config.unit_system.length === 'mi'`. +- Fallback `5` при чтении отсутствующего/некорректного `cell_cm` встречается в + frontend, static renderer, optimizer и backend validation. Это legacy + compatibility, а не default создания, и меняться не должно. + +## 4. Термины и эталонная эквивалентность + +- **Эталонный масштаб** — `REFERENCE_CELL_CM = 5`; его внешний вид сохраняется. +- **Физически эквивалентные пространства** — все координаты и размеры в plan + units второго пространства умножены на `k = 5 / cell_cm`, а физические поля в + cm не изменены. Layout-позиции, backdrop/decor geometry и camera centre/extent + преобразованы тем же `k`. +- **Одинаковый camera state** — одинаковый viewport и одинаковый относительный + zoom/pan; world-space view второго пространства преобразован через `k`. +- **Разрешённое различие** — число видимых/доступных узлов сетки на физический + метр и вызванная этим точность snap. +- **Visual unit** — legacy SVG-константа, чей нынешний размер при + `cell_cm: 5` является эталоном и должен следовать `k`. + +## 5. Границы задачи + +### 5.1 Входит + +- единый pure helper масштаба visual units; +- полный аудит SVG-примитивов full/static/embedded renderers; +- проёмы вместе с hover/hit geometry и door/gate lock badge position; +- Plan editor: saved outlines, previews, snap/repair/alignment/dimension chrome; +- Devices и Background editor chrome как регрессионные поверхности; +- скрытая изометрия, включая wall/floor height, openings и shadows; +- metric/imperial default и единицы поля при создании/редактировании; +- manual create и floors-import; +- светлая/тёмная тема, desktop и touch safety floor; +- документация, EN/RU i18n, оба changelog и целевые тесты. + +### 5.2 Не входит + +- изменение `GRID_N`, `GRID_PITCH`, координатной модели или snap algorithm; +- миграция/перезапись существующих `cell_cm`; +- изменение legacy fallback `5` для отсутствующего значения; +- изменение физических размеров стен, decor, мебели, Glow или пользовательских + icon/label multipliers; +- изменение зум-контракта: visual units продолжают масштабироваться вместе с + планом так же, как при `cell_cm: 5`, а не становятся screen-fixed; +- новый UI включения изометрии; +- постоянные размерные подписи из #52; +- расширение поддерживаемого диапазона `cell_cm` 0.1…1000. + +## 6. Классификация размеров + +Каждый затронутый размер относится ровно к одному классу. + +| Класс | Примеры | Правило | +|---|---|---| +| Physical | wall/partition/column cm, decor width/text, furniture, Glow radius, hatch #230, snap-node 5/10 cm | Уже переводится через `cell_cm`; не домножать | +| Screen | HTML labels/icons, `vector-effect: non-scaling-stroke`, px-derived hit/handles, vacuum trail | Оставить screen-fixed | +| Plan-relative | device/room-label size через `iconUnit`, content padding как доля frame, room corner radius как доля geometry | Масштабируется своей геометрией; не домножать | +| Visual unit | legacy raw SVG constants, эталонные при `cell_cm: 5` | Домножить на `visualScale(cell_cm)` | +| Grid | pitch, adaptive density, snap quantum | Не домножать; это единственное ожидаемое различие | + +Слепой множитель на весь SVG запрещён: он повторно увеличит physical и +plan-relative размеры. + +## 7. Контракт visual units + +Новый pure module, рекомендуемое имя `src/grid-scale.ts`: + +```ts +export const GRID_VISUAL_REFERENCE_CELL_CM = 5; + +export function gridVisualScale(cellCm: unknown): number; +export function gridVisualUnits(baseUnits: number, cellCm: unknown): number; +export function newSpaceCellCm(imperial: boolean): number; +``` + +### 7.1 `gridVisualScale` + +- валидное положительное `cellCm`: `5 / cellCm`; +- `5` возвращает ровно `1` без накопления погрешности; +- `NaN`, infinity, `0`, отрицательное и нечисловое значение дают `1`, то есть + legacy fallback 5; +- отдельного clamp нет: поддерживаемый диапазон уже ограничен 0.1…1000, а clamp + нарушил бы эквивалентность на законных значениях. + +### 7.2 Применение в CSS и SVG + +Full card и static card выставляют на корне соответствующего пространства +unitless custom property `--hp-cell-visual-scale`. Scale-sensitive CSS lengths +используют её; геометрические attributes и TypeScript-расчёты вызывают +`gridVisualUnits()`. + +Fallback custom property равен `1`, чтобы отдельный renderer или старый тестовый +host без переменной сохранял исторический `cell_cm: 5` вид. + +Фактор вычисляется один раз на пространство/рендер. Запрещено заново обходить +геометрию или делать plan-wide polyclip ради визуального масштаба. + +### 7.3 Инвариант `cell_cm: 5` + +При `cell_cm: 5` итоговые численные geometry attributes и computed styles +должны совпасть с состоянием до #239. Замена `2.5` на выражение, которое в +Chromium вычисляется не в те units, считается регрессией даже при близком +внешнем виде. + +## 8. Контракт поверхностей + +### 8.1 Flat View + +- room borders, wall-body outline, open boundaries и room hover сохраняют + эталонную толщину/halo; +- door/window/gate leaf, arc, glass, jamb и passage boundaries сохраняют + геометрию и толщину; +- opening lock badge остаётся на том же относительном отступе от стены; +- sun/Glow/device/value badge/room label/vacuum сохраняют текущий контракт без + повторного scale. + +### 8.2 Opening interaction + +- `openingVisibleMetrics()` масштабирует только fallback/padding visual units; + физический `jambHalf` от толщины стены остаётся physical; +- outline и hit rectangle масштабируются тем же factor, включая corner radius; +- hover появляется и click/double-click/drag срабатывают во всей той же видимой + области на экране, что при эквивалентном `cell_cm: 5`; +- preview и committed symbol читают один helper и не расходятся. + +### 8.3 Plan editor + +- контуры, active/draft/preview/repair/alignment lines, vertices и orphan/ + opening-preview markers сохраняют эталонный экранный footprint при одинаковом + относительном zoom; +- физические 5/10-см snap nodes остаются физическими; их screen-fixed strokes + остаются screen-fixed; +- dimension lines/ticks с `vector-effect` и HTML labels не получают второй + scale; +- grid pitch не меняется. При одинаковой физической длине метрический 1-см план + содержит в пять раз больше snap intervals, чем 5-см план. + +### 8.4 Devices и Background + +- marker, room label, LQI/value/lock satellites и capsules сохраняют размер и + pointer/action area; +- decor/furniture physical geometry не домножается; +- backdrop/decor/resize/physical selection frames и handles сохраняют текущий + screen-space либо px-derived контракт; +- ни один editor gesture, pointer capture, pan/pinch/cancel path не меняется. + +### 8.5 Static/embedded card + +`renderSpaceStatic()` выставляет тот же factor и использует те же opening +metrics/styles. Одинаковая fixture не может быть правильной в full card и иной +в `houseplan-space-card`. + +### 8.6 Скрытая изометрия + +- wall height, floor-edge depth, opening height/basis и geometry-dependent + shadow offsets умножаются на factor; +- SVG blur/translate в user units масштабируется так же; screen-fixed strokes + с `vector-effect` остаются без дополнительного множителя; +- factor входит в geometry fingerprint/cache key через фактически переданные + высоты; warm remount не может вернуть geometry другого `cell_cm`; +- icon/label projection и actions остаются прежними; +- Labs-функция остаётся скрытой и публичного переключателя не получает. + +## 9. Новый default и imperial projection + +### 9.1 Хранение + +`cell_cm` остаётся canonical числом сантиметров на клетку. + +- новый metric draft: `1`; +- новый imperial draft: `2.54` (ровно 1 inch); +- manual create и каждый элемент floors import используют один + `newSpaceCellCm(this._imperial)`; +- save продолжает записывать canonical cm. + +### 9.2 Поле диалога + +- metric: значение в cm, подпись «см на клетку» / `cm per cell`; +- imperial: значение в inches, подпись «дюйм на клетку» / `in per cell`; +- ввод inches переводится в canonical cm до validation/save; +- min/max в поле также проецируются в текущую систему единиц. + +### 9.3 Lossless edit + +Открытие и сохранение существующего пространства без изменения поля не меняет +его canonical `cell_cm`, в том числе в imperial HA. Округлённая строка UI не +становится новым значением сама по себе: draft хранит исходное canonical число +и отдельный признак/текст пользовательского редактирования либо эквивалентный +lossless механизм. + +Отсутствующий `cell_cm` продолжает читаться и при save materialize как 5 см. +Новый default нельзя использовать как read fallback. + +## 10. Данные, migration, i18n, a11y, privacy и security + +- Schema и формат данных не меняются; миграции нет. +- Новые EN/RU строки нужны для imperial unit label; существующий metric текст + сохраняется. +- Поле остаётся обычным label+number input; доступное имя и порядок фокуса не + меняются. +- Никаких новых данных HA, service calls, URL или файловых операций. +- Privacy/security без изменений. + +## 11. Touch contract + +View и kiosk остаются полностью поддержанными. Editors остаются desktop-first, +но safety floor из `docs/TOUCH-SUPPORT.md` обязателен: + +- opening hit/hover/action area не уменьшается из-за `cell_cm`; +- pan, pinch, second pointer, `pointercancel` и suppressed click ничего не + сохраняют и не запускают; +- screen-derived handles не переводятся ошибочно в physical/visual units; +- никаких новых hover-only действий. + +## 12. Performance + +- допустим один scalar factor на пространство и дешёвые умножения при render; +- новые plan-wide обходы, boolean geometry, layout reads и HA state dependencies + запрещены; +- существующие structural caches сохраняются; iso fingerprint различает + фактические scaled heights; +- large-house benchmark и hidden-iso benchmark проверяются перед бетой по + общему release-процессу, не в цикле реализации. + +## 13. Acceptance criteria + +**AC1.** `gridVisualScale(5) === 1`; для 1, 2.54, 10 и 25 результат равен +`5 / cell_cm`; invalid input даёт 1. `gridVisualUnits(base, cell)` применяет +ровно тот же factor. + +**AC2.** Физически эквивалентная flat View fixture при `cell_cm: 5` и +`cell_cm: 1` в одном viewport и camera state имеет одинаковые browser pixels +для комнат, masonry, partition/column, door/window/gate/passage, decor, +devices/labels, Glow и sun. Grid во View отсутствует. + +**AC3.** Opening leaf/arc/glass/jamb, outline и hit rectangle дают одинаковые +screen bounding boxes/stroke widths на эквивалентных планах. Hover, click, +double-click и drag срабатывают в одинаковой внешней области; preview и +committed symbol совпадают. + +**AC4.** Plan editor после маскирования grid paint даёт одинаковый raster/DOM +footprint для saved outlines, active/draft/thick preview, vertices, +snap/repair/alignment guides, opening preview и dimensions. При этом raw grid +содержит 100 intervals на метр при 1 см и 20 при 5 см. + +**AC5.** Physical snap nodes остаются диаметром 10/20 см, decor/furniture stroke +и size остаются заданными cm, hatch остаётся 9.6 см по #230, Glow radius не +меняется, а device/room-label size продолжает вычисляться через `iconCqw`. +Тест обязан падать при повторном применении visual factor к любому из этих +контрольных путей. + +**AC6.** Devices и Background editor selection frames, visible knobs и +screen-derived hit handles имеют одинаковый screen footprint на эквивалентных +планах; pointer sequence не меняет сохранённую geometry без положенного commit. + +**AC7.** `houseplan-space-card` на тех же двух fixtures совпадает по pixels и +critical DOM metrics, включая rooms, wall body и opening symbols. + +**AC8.** Скрытая изометрия при 1 и 5 см совпадает по wall/floor/opening height, +shadow extent и raster; projected icons/labels остаются над теми же объектами. +Light/dark проверяются отдельно. Isometric toggle остаётся Labs-only. + +**AC9.** Ручное создание нового пространства показывает и сохраняет: +metric — `1 cm/cell`, imperial — `1 in/cell` и canonical `cell_cm: 2.54`. + +**AC10.** Floors import использует те же defaults для каждого создаваемого +пространства и не возвращается к hardcoded 5. + +**AC11.** Existing `cell_cm: 5`, произвольное дробное значение и legacy space +без поля при open→save без редактирования не получают новый default и не +дрейфуют при imperial projection. Legacy missing value материализуется как 5. + +**AC12.** Metric/imperial label, input value, min/max и conversion соответствуют +системе единиц; переключение языка не меняет canonical draft. + +**AC13.** На `cell_cm: 5` все scale-sensitive geometry attributes и computed +styles совпадают с pre-#239 contract; current golden не требует принятия +необъяснённых изменений. Изменение screenshot диалога создания из 5 в 1 — +ожидаемый отдельный user-visible diff. + +**AC14.** Ни View/kiosk tap, ни editor pan/pinch/pointercancel не получают новых +действий; opening action area не меньше эталонной. + +**AC15.** Новых plan-wide geometry passes и state-dependent cache invalidations +нет; factor вычисляется O(1) на пространство. + +**AC16.** EN/RU user guide и оба changelog объясняют: внешний вид не зависит от +шага сетки, default новых пространств 1 см/1 дюйм, существующие значения не +мигрируют. + +## 14. План автотестов + +### 14.1 Unit + +- новый `test/grid-scale.test.mjs`: AC1, AC9–AC12; +- opening-symbol tests: scaled leaf/jamb/padding/hit metrics и отсутствие + double-scale physical jamb; +- iso projection/geometry tests: scaled height/depth/cache fingerprint; +- policy assertions на physical/screen control paths из AC5. + +### 14.2 Browser smoke + +Новый `demo/smoke_grid_scale_invariance.mjs` строит пару из одной rich fixture: + +- `cell_cm: 5`, reference coordinates; +- `cell_cm: 1`, все plan/layout/camera coordinates ×5, physical cm без изменений. + +Один прогон проверяет flat View light/dark, Plan states с grid mask, +Devices/Background selection, static card и hidden iso. Он сравнивает реальные +browser pixels через canvas и critical DOM metrics, а не только наличие классов. +Разрешённая tolerance должна быть нулевой либо обоснованной только +subpixel-antialiasing; она не может скрывать систематическую разницу толщины. + +Отдельный `demo/smoke_space_scale_defaults.mjs` проверяет manual create, +floors-import, metric/imperial labels, canonical save и lossless existing edit. + +Существующие целевые smokes запускаются по изменённым символам: opening preview/ +measure, plan snap, physical editor, decor, static card и isometric Stage 2/3. + +### 14.3 Golden и performance + +В цикле реализации — только typecheck, unit, build и named smokes. Полный golden, +smoke suite и performance — перед бетой по каноническому процессу. Golden +baseline разработчик не принимает. Public docs capture переснимается в задаче, +потому что create-dialog законно меняет default. + +## 15. Mutation gate + +| id | Что ломает | Гвард | +|---|---|---| +| `grid-scale-visual-factor-constant` | factor всегда 1 | unit + pixel smoke | +| `grid-scale-visual-factor-inverted` | `cell/5` вместо `5/cell` | unit + pixel smoke | +| `grid-scale-opening-symbol-unscaled` | leaf/jamb visual constants остаются raw | opening unit/smoke | +| `grid-scale-opening-hit-unscaled` | outline/hit padding остаётся raw | interaction smoke | +| `grid-scale-plan-chrome-unscaled` | Plan line/node visual unit не следует factor | Plan pixel smoke | +| `grid-scale-static-factor-missing` | static root не получает factor | static comparison | +| `grid-scale-iso-height-unscaled` | iso height/depth остаются constants | iso unit/pixel smoke | +| `grid-scale-metric-default-five` | metric create снова 5 | default smoke | +| `grid-scale-imperial-default-wrong` | imperial create хранит 1 cm либо 5 cm | unit/default smoke | +| `grid-scale-legacy-fallback-one` | read fallback меняется с 5 на новый default | compatibility unit | +| `grid-scale-imperial-roundtrip-drift` | rounded UI string перезаписывает canonical value | default smoke | +| `grid-scale-physical-double-scaled` | wall/decor/Glow/snap physical path получает второй factor | AC5 unit/smoke | + +## 16. Риски и защита + +| Риск | Вероятность / ущерб | Защита | +|---|---|---| +| Общий CSS factor повторно масштабирует physical элементы | средняя / высокий | классификация §6, negative controls AC5 | +| `px`/unitless `calc()` в SVG вычислится не в user units | средняя / высокий | exact `cell_cm:5` DOM + raster и pair pixel smoke | +| Hover визуально совпадёт, а hitbox останется маленьким | высокая / высокий | AC3 проверяет реальные pointer points по краю capsule | +| Imperial open/save округлит 5 см в 5.0038 см | высокая / средний | untouched canonical draft + AC11/mutant | +| Default поменяется только в одном create path | высокая / средний | manual + floors-import AC9/AC10 | +| Изометрический cache вернёт старую высоту | средняя / высокий | scaled input в fingerprint + warm-remount test | +| Dynamic grid затруднит pixel comparison Plan | высокая / низкий | grid paint маскируется, density проверяется отдельно | +| Широкий CSS audit изменит `cell_cm:5` pixels | средняя / высокий | reference exactness AC13, без baseline acceptance | +| Touch target станет physical вместо screen-equivalent | средняя / высокий | edge pointer smoke + TOUCH-SUPPORT safety floor | + +## 17. Release-артефакты + +`User-Visible: yes`: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в одном продуктовом коммите; +- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`: scale field/default и + визуальный инвариант; +- `docs/CANVAS.md`: taxonomy размеров и новый default; убрать утверждения + «default 5» там, где речь именно о новых пространствах, сохранив legacy + fallback; +- `docs/ARCHITECTURE.md`: актуализировать описание `cell_cm` default; +- `docs/TESTING.md`: named smoke и ручная матрица 1/5 cm + imperial; +- public docs screenshot create-space переснять через `demo/docs/capture.mjs`; +- `docs/images/screenshots.json` обновить тем же capture; +- golden/performance artifacts — только в предрелизном прогоне. + +Три поставляемых `houseplan-card.js` собираются и коммитятся вместе с +реализацией, должны быть побайтно одинаковы. + +## 18. Откат + +Один revert продуктового коммита возвращает прежние visual constants и default +5. Миграции данных и обратной миграции нет: пространства, созданные во время +действия версии с `cell_cm: 1`/`2.54`, остаются валидными обычными пространствами +и после отката. + +## 19. Принятые предположения — технические, менять свободно + +1. Pure helper и CSS property названы `gridVisualScale` и + `--hp-cell-visual-scale`; reviewer может предложить другое имя без изменения + продукта. +2. Lossless imperial edit реализуется через canonical value + touched/display + draft. Эквивалентный механизм допустим, если AC11 доказан. +3. Pair screenshot smoke использует динамическую вторую fixture вместо новых + постоянных golden baselines: это проверяет именно инвариант и не требует + принятия эталонов разработчиком. +4. Scale-sensitive CSS меняется адресно по таблице §6. Общий transform всего + renderer запрещён независимо от удобства реализации. diff --git a/docs/specs/README.md b/docs/specs/README.md index 4dfc6bc5..deb153ac 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -65,6 +65,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#226](https://github.com/Matysh/houseplan-card/issues/226) Entity-marker не дублируется родительским HA-устройством | [226-entity-parent-dedup.md](226-entity-parent-dedup.md) | | [#223](https://github.com/Matysh/houseplan-card/issues/223) Optimize канонизирует координаты без floating-point шума | [223-optimize-coordinate-canonicalization.md](223-optimize-coordinate-canonicalization.md) | | [#228](https://github.com/Matysh/houseplan-card/issues/228) Надёжное рисование стен и операции с готовым контуром | [228-plan-drawing-problems.md](228-plan-drawing-problems.md) | +| [#239](https://github.com/Matysh/houseplan-card/issues/239) Масштаб сетки не меняет внешний вид плана; default 1 см/1 дюйм | [239-grid-scale-invariance.md](239-grid-scale-invariance.md) | ## P2