Прилипание сильнее кадровой сетки: наезда в семь миллисекунд больше нет

Клип прилипал к концу соседа, а потом округлялся к ближайшему кадру — и
уезжал с той самой границы, к которой только что прилип. Границы клипов
в монтаже лежат не на сетке: они приходят из исходников. Влево это наезд
в семь миллисекунд: ноль пикселей на экране, а в модели заказ на переход.
Человек ищет, откуда взялся кроссфейд там, где ничего не накладывается.

Замерено: дыра, раздвинутая ровно под клип, давала после перетаскивания
наезд 0.007383 с слева и дыру такого же размера справа.

Теперь решение одно и принимается один раз: прилипло — ставим ровно
туда, не прилипло — на ближайший кадр (PlaceTime). Тем же путём идут и
оба растягивания за край.

Схлопывание больше не округляет каждого соседа по отдельности: все едут
на точную величину сдвига, и расстояния между ними остаются как были.
Иначе связка разъезжалась — картинка садилась на границу, а её звук
уезжал на полкадра.

Сообщение о наезде меряет той же мерой, что и сборка: кусок короче
половины кадра она пропускает, и пугать переходом из него незачем.

Проверка tests/dropgap.sh, на прежней сборке падает.
This commit is contained in:
4aiman 2026-10-16 00:24:18 +03:00
commit b078700fc7
3 changed files with 197 additions and 11 deletions

133
tests/dropgap.sh Executable file
View file

@ -0,0 +1,133 @@
#!/bin/sh
# Клип, брошенный в дыру ровно по своему размеру, встаёт в неё без
# наезда.
#
# Границы клипов в монтаже лежат не на кадровой сетке: они приходят из
# исходников. Клип прилипал к концу соседа, а потом округлялся к
# ближайшему кадру — и уезжал с этой границы на полкадра. Влево это
# наезд в семь миллисекунд: на экране ноль пикселей, а в модели заказ на
# переход, и человек ищет, откуда взялся кроссфейд там, где ничего не
# накладывается.
#
# Монтаж строится числами: клип A с концом не на сетке, дыра ровно по
# длине клипа D, сам D в конце. D тащится в дыру. Результат читается из
# сохранённого проекта.
#
# Запуск (экран :99 поднят):
# tests/dropgap.sh [<путь к nlve>]
set -e
cd "$(dirname "$0")/.."
BIN=${1:-$PWD/nlve}
DISP=${NLVE_DISPLAY:-:99}
SRC=$PWD/test/поток/пара.nlve
PROJ=$PWD/test/поток/дыра-ровно.nlve
[ -f "$SRC" ] || { echo "нет монтажа $SRC"; exit 2; }
export DISPLAY=$DISP
reachable() { timeout 5 xdotool getdisplaygeometry >/dev/null 2>&1; }
if ! reachable; then
for p in $(pgrep -x Xvfb); do
A=$(tr '\0' '\n' < /proc/$p/cmdline | grep -A1 -x -- -auth | tail -1)
[ -f "$A" ] || continue
XAUTHORITY=$A; export XAUTHORITY
reachable && break
done
fi
reachable ||
{ echo "нет доступа к экрану $DISP: подними его (xvfb-run) и повтори"; exit 2; }
python3 - "$SRC" "$PROJ" <<'PY'
import json, sys, copy
d = json.load(open(sys.argv[1]))
V = next(t for t in d['tracks'] if t['name'] == 'V1')
A = next(t for t in d['tracks'] if t['name'] == 'A1')
ов, оа = copy.deepcopy(V['clips'][0]), copy.deepcopy(A['clips'][0])
def клип(обр, ид, группа, старт, длина, вход):
c = copy.deepcopy(обр)
c.update(id=ид, group=группа, tl_start=старт, duration=длина,
src_in=вход, fade_in=0.0, fade_out=0.0)
return c
шаг = 1001/60000
D = round(3.5/шаг)*шаг # длина клипа — ровно по кадрам
конецA = 10.000700 # а граница соседа — нарочно между кадрами
V['clips'] = [клип(ов, 'a_v', 'ga', 0.0, конецA, 100.0),
клип(ов, 'b_v', 'gb', конецA + D, 10.0001, 200.0),
клип(ов, 'd_v', 'gd', 30.0, D, 300.0)]
A['clips'] = [клип(оа, 'a_a', 'ga', 0.0, конецA, 100.0),
клип(оа, 'b_a', 'gb', конецA + D, 10.0001, 200.0),
клип(оа, 'd_a', 'gd', 30.0, D, 300.0)]
json.dump(d, open(sys.argv[2], 'w'), ensure_ascii=False)
print(' дыра %.6f..%.6f, клип длиной %.6f с — ровно по размеру'
% (конецA, конецA + D, D))
PY
for p in $(pgrep -x nlve); do
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
done
sleep 1
SP=$PWD/test/поток/профиль-дыра-ровно
rm -rf "$SP"; mkdir -p "$SP"
rm -f "$PROJ.autosave"
XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP \
setsid "$BIN" "$PROJ" >/dev/null 2>&1 < /dev/null &
sleep 14
X="timeout 8 xdotool"
WID=
for w in $($X search --name "дыра-ровно.nlve" 2>/dev/null); do
xwininfo -id "$w" 2>/dev/null | grep -q IsViewable && { WID=$w; break; }
done
[ -n "$WID" ] || { echo "живое окно редактора не найдено"; exit 2; }
$X windowmove "$WID" 0 0 windowsize "$WID" 1270 990
sleep 2
ROW=${NLVE_ROW:-505}
# Два щелчка колесом от себя: в окно должен попасть и конец монтажа, где
# лежит перетаскиваемый клип, и дыра.
$X mousemove 700 $ROW
$X keydown ctrl click 5 keyup ctrl; sleep 0.4
$X keydown ctrl click 5 keyup ctrl; sleep 1
# Тащим клип с конца к левому краю дыры. Целимся грубо: доводить должно
# прилипание, в том и смысл.
$X mousemove 1200 $ROW mousedown 1; sleep 0.4
$X mousemove 1000 $ROW; sleep 0.3
$X mousemove 850 $ROW; sleep 0.3
$X mousemove 815 $ROW; sleep 0.5
$X mouseup 1; sleep 1.5
$X key ctrl+s; sleep 2
for p in $(pgrep -x nlve); do
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
done
rm -f "$PROJ.autosave"
python3 - "$PROJ" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
плохо = 0
for t in d['tracks']:
кл = sorted(t['clips'], key=lambda c: c['tl_start'])
if len(кл) < 3:
continue
for a, b in zip(кл, кл[1:]):
зазор = b['tl_start'] - (a['tl_start'] + a['duration'])
# Спрос строгий, до микросекунд: прилипание на то и прилипание,
# чтобы край сел ровно на край. Промах в семь миллисекунд меньше
# половины кадра и в ролике ничего не меняет, но монтаж от этого
# перестаёт быть встык: наезд рисуется, пишется в строку
# состояния и пугает переходом, которого не просили.
if abs(зазор) > 1e-6:
print(' ПЛОХО: на %s между %s и %s %s %.6f с'
% (t['name'], a['id'], b['id'],
'наезд' if зазор < 0 else 'дыра', abs(зазор)))
плохо = 1
print(' %s: %s' % (t['name'],
' '.join('%s %.6f' % (c['id'], c['tl_start']) for c in кл)))
if плохо:
raise SystemExit(1)
print(' ok: клип сел в дыру вплотную, ни наезда, ни остатка')
PY