refactor: one function owns the junction geometry of a space (#229 r2 M1)

Дефект High-1 был одинаковым в двух местах — и в живом рисовании, и в
«Оптимизировать планы», — потому что каждый вызывающий собирал геометрию
примыканий сам. Ревью r2 справедливо заметило, что и защита получилась
однобокой: юнит и мутант сторожили только оптимизатор, а путь карты — тот, где
дефект и был виден пользователю, — не сторожил никто. Заплатка в виде второго
мутанта-близнеца оставила бы причину на месте: два списка координат, которые
обязаны совпадать, но ничем не связаны.

Поэтому геометрия переехала в `spaceMergeGeometry(space, { excludeDraftId })`:
один источник комнат, колонн и концов черновиков, одни координаты, одно место,
где можно ошибиться. Оба вызывающих теперь строчка вызова.

Покрытие идёт за причиной, а не за симптомом: три юнита в
`test/wall-merge.test.mjs` проверяют масштаб полигонов (включая комнаты в форме
x/y/w/h и комнату без геометрии), исключение активного черновика и сам T-стык к
середине стороны комнаты. Мутанты `partition-merge-rescales-rooms` и
`chain-merge-sees-own-draft` перенацелены на общий модуль и теперь краснеют для
обоих путей сразу: 2 и 1 падение, проверено применением патча.

Сценарий с комнатой в смоке пробовал — не взлетел: рисование в комнату
поднимает `_offerWallFaces`, и цепочка не завершается штатно. Ломать смок под
тест не стал, юниты общего модуля покрывают оба пути честнее.

Issue: #229
User-Visible: no
This commit is contained in:
Codex
2026-08-21 13:25:16 +03:00
committed by Codex
parent fe2fcef735
commit fab0c38ca9
10 changed files with 243 additions and 188 deletions
+50
View File
@@ -4,6 +4,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyOpeningMoves, EPS_JOIN, mergeCollinearPartitions, junctionAt, remapHostT,
spaceMergeGeometry,
} from '../test-build/wall-merge.js';
const PITCH = 10; // one grid pitch in test coordinates
@@ -210,3 +211,52 @@ test('issue 229 openings hosted elsewhere are left alone', () => {
assert.equal(moved, 0);
assert.equal(JSON.stringify(openings), before);
});
test('issue 229 space geometry keeps rooms in the coordinates partitions use', () => {
// Both callers used to build this themselves and both rescaled the polygon,
// so a junction on a room side was never found (CODE-REVIEW-229-r1 High-1,
// r2 Medium-1). One function now, checked in the units both paths share.
const space = {
rooms: [
{ id: 'r1', poly: [[0.1, 0.1], [0.5, 0.1], [0.5, 0.5], [0.1, 0.5]] },
{ id: 'r2', x: 0.6, y: 0.1, w: 0.2, h: 0.2 },
{ id: 'broken' },
],
wall_columns: [{ id: 'c1', center: [0.7, 0.7] }],
};
const geometry = spaceMergeGeometry(space);
assert.deepEqual(geometry.roomPolygons[0], [[0.1, 0.1], [0.5, 0.1], [0.5, 0.5], [0.1, 0.5]]);
assert.deepEqual(geometry.roomPolygons[1][0], [0.6, 0.1], 'x/y/w/h rooms come through too');
assert.equal(geometry.roomPolygons.length, 2, 'a room without geometry is skipped');
assert.deepEqual(geometry.columns, space.wall_columns);
});
test('issue 229 the chain being finished is not its own junction', () => {
const space = {
rooms: [],
room_drafts: [
{ id: 'active', points: [[0.1, 0.1], [0.3, 0.1]] },
{ id: 'other', points: [[0.6, 0.6], [0.8, 0.6]] },
],
};
assert.deepEqual(
spaceMergeGeometry(space).draftEnds,
[[0.1, 0.1], [0.3, 0.1], [0.6, 0.6], [0.8, 0.6]],
'without an active chain every draft anchors a node',
);
assert.deepEqual(
spaceMergeGeometry(space, { excludeDraftId: 'active' }).draftEnds,
[[0.6, 0.6], [0.8, 0.6]],
'the chain about to disappear does not hold a node',
);
});
test('issue 229 a junction on a room side survives, through the shared geometry', () => {
const partitions = [seg('p1', 100, 500, 300, 500), seg('p2', 300, 500, 500, 500)];
const space = { rooms: [{ id: 'r1', x: 100, y: 100, w: 400, h: 400 }] };
const result = mergeCollinearPartitions(partitions, {
pitch: PITCH, geometry: spaceMergeGeometry(space),
});
assert.equal(result.merged, 0, 'the middle of the room side holds the node');
assert.equal(result.partitions.length, 2);
});