mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
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
Refresh the shared wall-key fixture contract and keep generated frontend bundles synchronized with the fingerprinted geometry fixtures. Issue: #260 User-Visible: no
64 lines
4.0 KiB
JavaScript
64 lines
4.0 KiB
JavaScript
/**
|
||
* Ключ записи толщины стены — один на все фикстуры проекта.
|
||
*
|
||
* Копия формулы из `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)}`;
|
||
};
|