Панорама двигает сцену трансформом, viewBox пишется по бюджету

Перезапись `viewBox` — это не сдвиг, а инвалидация растеризации всей сцены.
Кадр жеста делал её каждый раз: в профиле владельца (Firefox 155, 144 Гц) кадр
доезжал до экрана 200 мс, а драйвер пропускал 124–144 тика в секунду с пометкой
«ждём краску».

Теперь `paintLiveViewport` держит якорь — кадр, чей `viewBox` записан в DOM, и
момент записи. Кадр жеста двигает узлы сцены тем же проективным преобразованием,
которым уже двигались HTML-слои, а `viewBox` переписывается по бюджету: 100 мс
либо 15 % сдвига/масштаба. Ни атрибут, ни стиль не пишутся, если строка не
изменилась.

Issue: #531
User-Visible: yes
This commit is contained in:
Codex
2026-09-11 14:23:48 +03:00
parent 9d35ab35ac
commit 58e31668c9
43 changed files with 11156 additions and 20 deletions
+40
View File
@@ -7601,6 +7601,46 @@ const MUTANT_DEFINITIONS = [
replace: ' projection.translateXPercent === 1',
}],
},
{
id: 'live-pan-rewrites-viewbox-every-frame',
guard: 'node demo/smoke_live_pan_viewbox.mjs',
because: '#531: перезапись `view' + 'Box` — это инвалидация растеризации всей сцены, её '
+ 'нельзя сдвинуть, её надо нарисовать заново; в профиле владельца кадр панорамы '
+ 'доезжал до экрана 200 мс, а драйвер пропускал 124-144 тика в секунду с пометкой '
+ '«ждём краску». Кадр жеста обязан быть трансформом, а не новой краской',
patches: [{
file: 'src/live-viewport.ts',
find: ' || !finiteView(base.frame.floor) || needsViewBoxRefresh(base, current, now);',
replace: ' || !finiteView(base.frame.floor) || true;',
}],
},
{
id: 'live-pan-writes-unchanged-viewbox',
guard: 'node --test test/live-viewport.test.mjs',
because: '#531: тихий кадр обязан оставлять DOM нетронутым. Запись того же значения в '
+ 'атрибут — это всё равно инвалидация стиля, и в обычном режиме узлы floor и camera '
+ 'получают ровно одну и ту же строку кадр за кадром',
patches: [{
file: 'src/live-viewport.ts',
find: " if (svg.getAttribute('view" + "Box') !== text) svg.setAttribute('view" + "Box', text);",
replace: " svg.setAttribute('view" + "Box', text);",
}],
},
{
id: 'live-pan-shift-threshold-ignored',
guard: 'node --test test/live-viewport.test.mjs',
because: '#531: временного бюджета мало — за 100 мс рывка план уезжает на пол-экрана, и '
+ 'на набегающем крае остаётся пустая полоса. Сдвиговый порог существует ровно ради '
+ 'этого случая и обязан срабатывать раньше времени',
patches: [{
file: 'src/live-viewport.ts',
find: ' if (Math.abs(after.x - before.x) >= after.w * LIVE_VIEWBOX_REFRESH_SHIFT)'
+ ' return true;\n'
+ ' if (Math.abs(after.y - before.y) >= after.h * LIVE_VIEWBOX_REFRESH_SHIFT)'
+ ' return true;\n',
replace: '',
}],
},
{
id: 'render-invalidation-unknown-key-ignored',
guard: 'node --test test/render-invalidation.test.mjs',