diff --git a/demo/smoke_real_plan_masonry.mjs b/demo/smoke_real_plan_masonry.mjs new file mode 100644 index 00000000..7642b62b --- /dev/null +++ b/demo/smoke_real_plan_masonry.mjs @@ -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); diff --git a/scripts/smoke-links.mjs b/scripts/smoke-links.mjs index 150644ad..780a2527 100644 --- a/scripts/smoke-links.mjs +++ b/scripts/smoke-links.mjs @@ -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', diff --git a/test/fixtures/real-plan-second-floor.json b/test/fixtures/real-plan-second-floor.json new file mode 100644 index 00000000..39ea73d0 --- /dev/null +++ b/test/fixtures/real-plan-second-floor.json @@ -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": [] + } +}