From ed700c8c8e161f71cd77889a2ed91cb27885d115 Mon Sep 17 00:00:00 2001 From: Matysh Date: Tue, 1 Sep 2026 23:13:59 +0300 Subject: [PATCH] fix: crop the documentation frames on whole pixels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замер снял главное: три снимка подряд в одном состоянии страницы совпадают побайтово у всех десяти сценариев. Значит рендер детерминирован, а плавает то, что приходит на вход съёмке. Обрезка считается из живого DOM через getBoundingClientRect и приходит дробной. Дробная обрезка заставляет Chromium ресемплить кадр — и тогда сдвиг раскладки на десятую пикселя переписывает границы всех элементов на единицы уровней. Ровно эта подпись в #410: 76 пикселей, максимум 2 уровня, alpha не тронута, всё на сглаженных границах полей. Рамка расширяется наружу, а не округляется к ближайшему: обрезка обязана содержать цель целиком. Issue: #410 User-Visible: no --- demo/docs/capture.mjs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/demo/docs/capture.mjs b/demo/docs/capture.mjs index 6fc9c0a6..5fcb4701 100644 --- a/demo/docs/capture.mjs +++ b/demo/docs/capture.mjs @@ -262,9 +262,35 @@ try { const runtime = await applyDocumentationState(page, scenario); if (scenario.expectDialog && !runtime.dialog) throw new Error(`documentation scenario did not open its dialog: ${scenario.id}`); - const clip = scenario.capture === 'room-card' + const rawClip = scenario.capture === 'room-card' ? await roomCardClip(page) : await goldenClip(page, scenario.capture); + // Обрезка выравнивается по целым пикселям (#410). + // + // Прямоугольник считается из живого DOM через getBoundingClientRect, то есть + // приходит дробным. Дробная обрезка заставляет Chromium ресемплить кадр, и + // тогда сдвиг раскладки на десятую пикселя — от метрики шрифта, от + // появившегося скроллбара — переписывает границы всех элементов на единицы + // уровней. Ровно эта подпись и была в #410: 76 пикселей, максимум 2 уровня, + // alpha не тронута, всё на сглаженных границах полей. + // + // Замер показал, где искать: три снимка подряд в одном состоянии страницы + // совпадают побайтово у всех десяти сценариев, а между прогонами плавают + // два. Значит дело не в рендере, а в том, что приходит на вход съёмке. + // + // Внешняя рамка расширяется, а не округляется к ближайшему: обрезка обязана + // содержать цель целиком, потерять полпикселя по краю нельзя. + const clip = rawClip && { + x: Math.floor(rawClip.x), + y: Math.floor(rawClip.y), + width: Math.ceil(rawClip.x + rawClip.width) - Math.floor(rawClip.x), + height: Math.ceil(rawClip.y + rawClip.height) - Math.floor(rawClip.y), + }; + if (STABILITY_SHOTS && rawClip) { + console.log(`${scenario.id} обрезка: сырая` + + ` ${rawClip.x},${rawClip.y} ${rawClip.width}x${rawClip.height}` + + ` → целая ${clip.x},${clip.y} ${clip.width}x${clip.height}`); + } // Два кадра ожидания перед съёмкой — как в golden. `animations: 'disabled'` // гасит анимации, но не гарантирует, что уже запланированный ре-рендер // успел лечь в композитор до захвата.