Files
houseplan-card/demo/fixtures/wall-key.mjs
ClaudeandSergey Matyunin c14dcecb98
Validate / docs (push) Failing after 25s
Validate / process-workflow-sync (push) Successful in 32s
Validate / provenance (push) Successful in 42s
Validate / changes (push) Successful in 32s
Validate / process-gate (push) Failing after 41s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / reuse (push) Successful in 46s
Validate / backend (push) Skipped
Validate / frontend (push) Successful in 9m15s
Validate / smoke (1) (push) Failing after 18m47s
Validate / smoke (2) (push) Failing after 19m9s
Validate / smoke (3) (push) Failing after 18m12s
Validate / smoke_done (push) Skipped
Validate / golden (push) Failing after 20m2s
Validate / performance_smoke (push) Failing after 25m25s
test(fixtures): write the real wall key in one place
Refresh the shared wall-key fixture contract and keep generated frontend bundles synchronized with the fingerprinted geometry fixtures.

Issue: #260
User-Visible: no
2026-08-24 15:06:06 +03:00

64 lines
4.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Ключ записи толщины стены — один на все фикстуры проекта.
*
* Копия формулы из `src/wall-thickness.ts`, и копия здесь неизбежна. Фикстуры
* обязаны оставаться без внешних импортов: бэкенд-гейт запускает их как
* `node --input-type=module --eval "import * as f from './demo/fixtures/…'"`
* в job без `npm ci` и без `test-build/` (`.github/workflows/validate.yml`,
* job `backend`), а `scripts/source-fingerprint.mjs` хеширует только `src/**`
* и `.mjs` из `demo/fixtures` и `demo/golden` — код, втянутый из `scripts/`,
* менял бы поведение фикстуры при неизменном отпечатке, на котором стоят и
* валидность golden-эталонов, и переиспользование гейтов.
*
* Поэтому файл лежит ЗДЕСЬ, внутри `demo/fixtures`: так он попадает в
* отпечаток, и так его видит одна привязка вместо трёх копий формулы.
* `test/fixture-wall-key.test.mjs` сверяет его с продуктовым `wallKey`.
*
* Ловушка, из-за которой этот файл и появился (#260): точность зависит от шага.
* При `pitch = 1/240` продукт печатает ШЕСТЬ знаков, а не четыре — фикстура с
* четырьмя расходилась с продуктом на каждой записи и находилась только через
* терпимый запас `lookupWall`. Метка вместо ключа (`perf-wall-0-3`) не
* находилась вовсе: все сплошные рёбра оставались с нулевой толщиной.
*/
/** Шаг решётки редактора в нормализованных координатах (`GRID_N = 240`). */
export const WALL_KEY_PITCH = 1 / 240;
/** Направление стены по модулю 180°: стена одна и та же с любого конца. */
const direction = (a, b) => {
let dx = b[0] - a[0], dy = b[1] - a[1];
const length = Math.hypot(dx, dy);
if (length < 1e-12) return [1, 0];
dx /= length; dy /= length;
if (dx < -1e-12 || (Math.abs(dx) <= 1e-12 && dy < 0)) return [-dx, -dy];
return [dx, dy];
};
/**
* Координата, отличающаяся от узла решётки не больше точности хранения, — это
* тот же узел (#258). Канонизация опознания, а не снап геометрии: произвольная
* точка вне решётки остаётся вне решётки. Без этого шага ничья округления на
* стене нечётной длины в шагах разводила один и тот же ключ на два.
*/
const keyEpsilon = (pitch) => Math.max(Math.abs(pitch) * 1e-6, 1e-9);
const canonical = (value, pitch) => {
if (!(pitch > 0) || !Number.isFinite(value)) return value;
const snapped = Math.round(value / pitch) * pitch;
return Math.abs(snapped - value) <= keyEpsilon(pitch) ? snapped : value;
};
export const fixtureWallKey = (a, b, pitch = WALL_KEY_PITCH) => {
const quantise = (value) => (pitch > 0 && Number.isFinite(value)
? Math.round(value / pitch) * pitch : value);
const ca = [canonical(a[0], pitch), canonical(a[1], pitch)];
const cb = [canonical(b[0], pitch), canonical(b[1], pitch)];
const mx = quantise((ca[0] + cb[0]) / 2);
const my = quantise((ca[1] + cb[1]) / 2);
const [dx, dy] = direction(ca, cb);
let angle = Math.atan2(dy, dx);
if (angle < 0) angle += Math.PI;
const bucket = Math.round(angle * 1800) / 1800;
const precision = pitch > 0 && pitch < 0.01 ? 6 : pitch < 1 ? 4 : 2;
return `${mx.toFixed(precision)},${my.toFixed(precision)}@${bucket.toFixed(4)}`;
};