test(smoke): masonry of a real plan must not break

Eight closed issues on wall junctions — #271, #272, #275, #276, #277, #278,
\#279, #280 — shipped in beta.9, and the break in the owner's real plan
survived. Every one of them was accepted on synthetic fixtures: a cell-5
mixed-depth T, a rectilinear T with three equal half-depths. On those the fixes
work.

The smoke asks the product itself, through isPointInFill on the wall path,
whether masonry exists where the model promises it. That is independent of both
resolution and the pixel-diff thresholds which miss this class: a 45-step break
on a large plan is a fraction of a per cent of the frame, well under the 0.05
per cent scene tolerance.

Measured on the shipped code: four breaks, 181 grid steps in total, 45.25 each.
That number is derivable rather than incidental — the node joins a 30 cm
exterior wall, a 30 cm spur and an arm five steps long; half-depth 15,
MITRE_LIMIT 4, corridor radius 60, and 60 − 15 = 45 steps are cut out of the
neighbouring 20 cm wall. The corridor eats the masonry, which is what #271 and
\#275 describe.

The fixture is privacy-minimised — neutral room names and ids, no device
bindings — and keeps its coordinates, because they are the point. It lives in
test/fixtures rather than demo/fixtures on purpose: sourceFingerprint hashes the
.mjs of demo/fixtures and demo/golden, so anything added there staleizes the
committed bundle and the screenshot manifest at once. Verified after this
commit: bundle still fresh, screenshots still current.

The debt is recorded as numbers and compared exactly. Better and the test asks
for the numbers to be updated, which proves the improvement; worse and it
catches the regression. Verified by execution in both directions.

Issue: #285
User-Visible: no
This commit is contained in:
Claude
2026-08-24 13:02:56 +03:00
parent d8696315eb
commit c654e0ec42
3 changed files with 665 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
// Кладка реального плана не должна прерываться (#285).
//
// Зачем это отдельно от golden и от синтетических фикстур. Восемь задач подряд
// (#271, #272, #275, #276, #277, #278, #279, #280) прошли ТЗ, ревью, golden и
// смоки — и разрыв кладки на реальном плане владельца остался. У всех восьми
// критерии приёмки стояли на синтетике: «cell-5 mixed-depth T», «прямоугольный
// T с тремя равными полутолщинами». На ней фиксы работают.
//
// Здесь проверяется другое: берётся геометрия настоящего этажа и спрашивается
// единственное — есть ли кладка там, где по модели она обязана быть. Ответ даёт
// сам продукт через `isPointInFill` на пути стены, поэтому проверка не зависит
// ни от разрешения, ни от порогов пиксельного сравнения, которые этот класс
// дефектов пропускают.
//
// В фикстуре намеренно нет проёмов: тогда правило не требует никакой
// бухгалтерии — КАЖДАЯ точка на осевой линии ребра комнаты обязана лежать
// внутри кладки.
import { readFileSync } from 'node:fs';
import { launch, checkAll, finish } from './serve.mjs';
const FIXTURE = new URL('../test/fixtures/real-plan-second-floor.json', import.meta.url);
const { space } = JSON.parse(readFileSync(FIXTURE, 'utf8'));
/**
* Признанный долг beta.9, ведётся в #284 вместе с #271/#275.
*
* Четыре разрыва — это две стены, посчитанные с двух сторон каждой общей пары:
* «Элина | Холл» и «Холл | Кабинет» в исходных названиях.
*
* 45.25 шага на разрыв — не случайное число. В узле сходятся наружная стена
* 30 см, выступ 30 см и луч длиной ВСЕГО 5 шагов. Для 30 см полутолщина 15
* шагов, `MITRE_LIMIT = 4`, радиус коридора 4 × 15 = 60 шагов, и из соседней
* стены 20 см вырезается 60 − 15 = 45. Совпадение с измерением показывает, что
* съедает кладку именно коридор стыка.
*
* Числа проверяются точно. Станет лучше — тест потребует их обновить и тем
* самым докажет улучшение; станет хуже — поймает регрессию.
*/
const KNOWN_GAP_COUNT = 4;
const KNOWN_GAP_STEPS = 181;
const { page, browser } = await launch();
const out = {};
const measured = await page.evaluate(async (space) => {
const card = window.__card;
card._serverCfg = { spaces: [space], markers: [], settings: {} };
card._cfgEpoch = (card._cfgEpoch || 0) + 1;
card._modelCache = null;
card._space = space.id;
card._setMode?.('view');
card.requestUpdate();
await card.updateComplete;
await new Promise((done) => setTimeout(done, 250));
card._fitAll?.();
card.requestUpdate();
await card.updateComplete;
await new Promise((done) => setTimeout(done, 250));
const root = card.shadowRoot || card.renderRoot;
const wall = root.querySelector('[data-hp="wall"]');
if (!wall?.isPointInFill) return { error: 'wall path not found' };
const NORM = 1000;
const height = card._spaceH || NORM;
const at = (x, y) => new DOMPoint(x * NORM, y * height);
const STEP = 1 / 240;
// Шаг выборки — четверть шага решётки: мельче любого осмысленного разрыва и
// достаточно грубо, чтобы прогон оставался быстрым.
const SAMPLE = STEP / 4;
const gaps = [];
let sampled = 0;
let missing = 0;
for (const room of space.rooms) {
const poly = room.poly;
for (let i = 0; i < poly.length; i++) {
const a = poly[i];
const b = poly[(i + 1) % poly.length];
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
const count = Math.max(2, Math.round(length / SAMPLE));
let runStart = null;
for (let k = 0; k <= count; k++) {
const t = k / count;
const x = a[0] + (b[0] - a[0]) * t;
const y = a[1] + (b[1] - a[1]) * t;
sampled++;
const solid = wall.isPointInFill(at(x, y));
if (!solid) {
missing++;
if (runStart === null) runStart = t;
} else if (runStart !== null) {
gaps.push({ room: room.id, edge: i, steps: (t - runStart) * length / STEP });
runStart = null;
}
}
if (runStart !== null) {
gaps.push({ room: room.id, edge: i, steps: (1 - runStart) * length / STEP });
}
}
}
const worst = gaps.slice().sort((x, y) => y.steps - x.steps).slice(0, 5);
return {
sampled,
missing,
gapCount: gaps.length,
totalGapSteps: gaps.reduce((sum, g) => sum + g.steps, 0),
worst,
};
}, space);
console.log(JSON.stringify(measured, null, 2));
out.wallPathFound = !measured.error;
out.sampledEnough = measured.sampled > 5000;
out.gapCountMatchesKnownDebt = measured.gapCount === KNOWN_GAP_COUNT;
out.gapLengthMatchesKnownDebt = Math.abs(measured.totalGapSteps - KNOWN_GAP_STEPS) < 1;
if (!out.gapCountMatchesKnownDebt || !out.gapLengthMatchesKnownDebt) {
console.log(`Долг изменился: было ${KNOWN_GAP_COUNT} разрывов на ${KNOWN_GAP_STEPS} шага,`
+ ` стало ${measured.gapCount} на ${measured.totalGapSteps.toFixed(2)}.`
+ ' Меньше — обновите числа и приложите прогон; больше — это регрессия.');
}
checkAll(out);
await finish(browser, out);
+15
View File
@@ -27,6 +27,21 @@
/** @type {SmokeLink[]} */
export const SMOKE_LINKS = [
{
// #285: смок не произносит ни одного имени продуктового кода — он подставляет
// геометрию реального этажа и спрашивает сам продукт через isPointInFill,
// есть ли кладка там, где модель её обещает. Поиском по тексту такая связь
// не находится никогда.
symbols: [
'wallBodiesGeometry', 'wallEdgeBodies', 'buildMultiWallNodeMap',
'multiWallBevelTriangles', 'virtualJunctionPatches', 'linearWallJoinPatches',
'MITRE_LIMIT', 'MULTI_WALL_JOIN_LIMIT', 'insetContour', 'atomicPolyForRoom',
],
smokes: ['smoke_real_plan_masonry.mjs'],
because: 'на реальном плане кладка рвётся там, где синтетические фикстуры целы: '
+ 'восемь закрытых задач по стыкам вошли в beta.9, а разрыв в 45 шагов остался — '
+ 'пиксельные пороги golden такую долю кадра не видят',
},
{
symbols: [
'WallGeometryStatus', 'WallGeometryComponent', 'WallBodiesGeometryResult',
+527
View File
@@ -0,0 +1,527 @@
{
"note": "Privacy-minimised geometry of a real second floor (issue #285 (defect tracked in #284)). Room names and ids are neutral; only geometry, thickness and openings are meaningful. Coordinates are unchanged: they are the point of the fixture.",
"space": {
"id": "real-second-floor",
"title": "Real second floor",
"cell_cm": 1,
"view_box": [
0,
0,
1,
1
],
"settings": {
"show_borders": true,
"show_names": false,
"fill_mode": "custom",
"custom_fill": {
"c": "#607d8b",
"a": 0
},
"glow_enabled": false
},
"rooms": [
{
"id": "room-a",
"name": "Room A",
"poly": [
[
-1.670833333,
1.266666667
],
[
-1.670833333,
-0.208333333
],
[
0.4,
-0.208333333
],
[
0.4,
1.266666667
]
]
},
{
"id": "room-b",
"name": "Room B",
"poly": [
[
2.404166667,
-0.208333333
],
[
2.404166667,
1.266666667
],
[
0.4,
1.266666667
],
[
0.4,
-0.208333333
]
]
},
{
"id": "room-c",
"name": "Room C",
"poly": [
[
-1.670833333,
3.866666667
],
[
-1.670833333,
2.95
],
[
-0.354166667,
2.954166667
],
[
-0.354166667,
3.866666667
]
]
},
{
"id": "room-d",
"name": "Room D",
"poly": [
[
-1.670833333,
2.95
],
[
-1.670833333,
2.0875
],
[
-0.354166667,
2.0875
],
[
-0.354166667,
2.954166667
]
]
},
{
"id": "room-e",
"name": "Room E",
"poly": [
[
-1.670833333,
2.0875
],
[
-1.670833333,
1.266666667
],
[
-0.354166667,
1.266666667
],
[
-0.354166667,
2.0875
]
]
},
{
"id": "room-f",
"name": "Room F",
"poly": [
[
-0.354166667,
2.954166667
],
[
-0.354166667,
2.2875
],
[
1.058333333,
2.2875
],
[
1.058333333,
3.866666667
],
[
-0.354166667,
3.866666667
]
]
},
{
"id": "room-g",
"name": "Room G",
"poly": [
[
1.058333333,
3.866666667
],
[
1.058333333,
2.2875
],
[
2.404166667,
2.2875
],
[
2.404166667,
3.866666667
]
]
},
{
"id": "room-h",
"name": "Room H",
"poly": [
[
-0.354166667,
1.266666667
],
[
2.404166667,
1.266666667
],
[
2.404166667,
1.245833333
],
[
2.904166667,
1.245833333
],
[
2.904166667,
2.308333333
],
[
2.404166667,
2.308333333
],
[
2.404166667,
2.2875
],
[
-0.354166667,
2.2875
]
]
}
],
"walls": [
{
"key": "1.025000,1.266667@0.0000",
"cm": 20,
"a": [
-0.354166667,
1.266666667
],
"b": [
2.404166667,
1.266666667
]
},
{
"key": "1.025000,2.287500@0.0000",
"cm": 20,
"a": [
2.404166667,
2.2875
],
"b": [
-0.354166667,
2.2875
]
},
{
"key": "-0.637500,-0.208333@0.0000",
"cm": 30,
"a": [
-1.670833333,
-0.208333333
],
"b": [
0.4,
-0.208333333
]
},
{
"key": "-0.637500,1.266667@0.0000",
"cm": 20,
"a": [
0.4,
1.266666667
],
"b": [
-1.670833333,
1.266666667
]
},
{
"key": "1.404167,-0.208333@0.0000",
"cm": 30,
"a": [
0.4,
-0.208333333
],
"b": [
2.404166667,
-0.208333333
]
},
{
"key": "1.058333,3.079167@1.5706",
"cm": 20,
"a": [
1.058333333,
2.2875
],
"b": [
1.058333333,
3.866666667
]
},
{
"key": "2.404167,3.079167@1.5706",
"cm": 30,
"a": [
2.404166667,
2.2875
],
"b": [
2.404166667,
3.866666667
]
},
{
"key": "-1.670833,0.529167@1.5706",
"cm": 30,
"a": [
-1.670833333,
1.266666667
],
"b": [
-1.670833333,
-0.208333333
]
},
{
"key": "0.400000,0.529167@1.5706",
"cm": 20,
"a": [
0.4,
-0.208333333
],
"b": [
0.4,
1.266666667
]
},
{
"key": "2.404167,0.529167@1.5706",
"cm": 30,
"a": [
2.404166667,
-0.208333333
],
"b": [
2.404166667,
1.266666667
]
},
{
"key": "0.354167,3.866667@0.0000",
"cm": 30,
"a": [
1.058333333,
3.866666667
],
"b": [
-0.354166667,
3.866666667
]
},
{
"key": "1.733333,3.866667@0.0000",
"cm": 30,
"a": [
2.404166667,
3.866666667
],
"b": [
1.058333333,
3.866666667
]
},
{
"key": "-1.012500,2.954167@0.0033",
"cm": 20,
"a": [
-1.670833333,
2.95
],
"b": [
-0.354166667,
2.954166667
]
},
{
"key": "-1.012500,2.087500@0.0000",
"cm": 40,
"a": [
-1.670833333,
2.0875
],
"b": [
-0.354166667,
2.0875
]
},
{
"key": "-1.012500,3.866667@0.0000",
"cm": 30,
"a": [
-0.354166667,
3.866666667
],
"b": [
-1.670833333,
3.866666667
]
},
{
"key": "2.904167,1.775000@1.5706",
"cm": 30,
"a": [
2.904166667,
1.245833333
],
"b": [
2.904166667,
2.308333333
]
},
{
"key": "-0.354167,1.779167@1.5706",
"cm": 20,
"a": [
-0.354166667,
2.2875
],
"b": [
-0.354166667,
1.266666667
]
},
{
"key": "-1.670833,3.408333@1.5706",
"cm": 30,
"a": [
-1.670833333,
3.866666667
],
"b": [
-1.670833333,
2.95
]
},
{
"key": "-0.354167,3.412500@1.5706",
"cm": 20,
"a": [
-0.354166667,
2.954166667
],
"b": [
-0.354166667,
3.866666667
]
},
{
"key": "-0.354167,2.520833@1.5706",
"cm": 20,
"a": [
-0.354166667,
2.0875
],
"b": [
-0.354166667,
2.954166667
]
},
{
"key": "-1.670833,2.520833@1.5706",
"cm": 30,
"a": [
-1.670833333,
2.95
],
"b": [
-1.670833333,
2.0875
]
},
{
"key": "-1.670833,1.679167@1.5706",
"cm": 30,
"a": [
-1.670833333,
2.0875
],
"b": [
-1.670833333,
1.266666667
]
},
{
"key": "2.654167,1.245833@0.0000",
"cm": 30,
"a": [
2.404166667,
1.245833333
],
"b": [
2.904166667,
1.245833333
]
},
{
"key": "2.654167,2.308333@0.0000",
"cm": 30,
"a": [
2.904166667,
2.308333333
],
"b": [
2.404166667,
2.308333333
]
}
],
"partitions": [
{
"id": "partition-mt2on9ou-0",
"a": [
-1.670833333,
1.266666667
],
"b": [
-1.670833333,
3.866666667
],
"cm": 30
}
],
"openings": []
}
}