Клип встаёт на курсор, монтаж едет следом, сборка ждёт конца правки

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

Файл из списка падал в конец дорожки, стоило месту под ним оказаться
занятым, — а занято оно почти всегда: клип длиной в целый файл упирается
хвостом в то, что правее, даже когда начало легло в пустоту. Теперь
монтаж раздвигается: MakeRoomAt режет все дорожки в точке вставки и
сдвигает всё, что начинается с неё и правее, на длину нового клипа. Звук
и надписи едут вместе с картинкой, иначе синхрон рассыплется.

Точка вставки при этом округляется к кадровой сетке. Без этого вставка
давала наезд в пять миллисекунд: рез внутри раздвижки округляет время
сам, а клип клался на неокруглённый курсор. Отсюда и был «кроссфейд без
видимого перекрытия».

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

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

Проверки числами: раздвижка монтажа и прилипание хвостом. С окном:
insertat.sh, dragscroll.sh, genquiet.sh. Все три прогнаны и на прежней
сборке, где падают: клип в конце дорожки вместо курсора, 0.667 с вместо
4.238 с, сборка через 0.20 с вместо 1.62 с.
This commit is contained in:
4aiman 2026-10-15 23:33:58 +03:00
commit 0fa9bb6130
6 changed files with 1068 additions and 77 deletions

143
tests/dragscroll.sh Executable file
View file

@ -0,0 +1,143 @@
#!/bin/sh
# Монтаж едет следом, пока тащишь клип у кромки окна.
#
# При большом увеличении видно секунды, а двигать кусок надо на минуты.
# Если монтаж не прокручивается, клип упирается в край окна и дальше не
# идёт: приходится отпускать, прокручивать, брать снова — и так десятки
# раз.
#
# Проверяется так: монтаж из одного клипа, сильное увеличение, клип
# тащится к правому краю и там удерживается на месте. Рука стоит, значит
# двигать может только прокрутка. Результат читается из сохранённого
# проекта, а не с картинки.
#
# Запуск (экран :99 поднят):
# tests/dragscroll.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; }
# Невидимый экран поднят через xvfb-run, у него свой файл доступа, а в
# окружении обычно лежит файл рабочего экрана. С чужим файлом xdotool не
# видит ни одного окна, и проверка «проходит» вслепую, ничего не проверив.
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
d = json.load(open(sys.argv[1]))
keep = None
for t in d['tracks']:
for c in t['clips']:
if c['kind'] == 'video' and c['tl_start'] < 0.001:
keep = c.get('group')
for t in d['tracks']:
t['clips'] = [c for c in t['clips'] if c.get('group') == keep]
json.dump(d, open(sys.argv[2], 'w'), ensure_ascii=False)
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
rm -rf test/поток/профиль-прокрутка
mkdir -p test/поток/профиль-прокрутка
rm -f "$PROJ.autosave"
SP=$PWD/test/поток/профиль-прокрутка
XDG_CONFIG_HOME=$SP XDG_CACHE_HOME=$SP XDG_DATA_HOME=$SP DISPLAY=$DISP \
setsid "$BIN" "$PROJ" >/dev/null 2>&1 < /dev/null &
sleep 14
X="timeout 8 xdotool"
# Окно ищем по имени монтажа и проверяем, что оно на экране. По одному
# «nlve» находятся и обломки прошлых запусков: они невидимы, нажатия в
# них уходят в никуда, и проверка молча показывает «ничего не двинулось».
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
start() {
$X key ctrl+s
sleep 2
python3 -c "
import json
d = json.load(open('$PROJ'))
for t in d['tracks']:
for c in t['clips']:
if c['kind'] == 'video':
print('%.3f' % c['tl_start']); raise SystemExit
print('0')
"
}
echo " начало клипа до правок: $(start) с"
ROW=${NLVE_ROW:-505} # высота видеодорожки в окне
GRAB=${NLVE_GRAB:-650} # точка захвата внутри клипа (клип начинается с 588)
EDGE=1255 # кромка у правого края окна
# Увеличение: шесть щелчков колесом с Ctrl, опора в точке захвата, чтобы
# клип из-под указателя не уехал.
I=0
while [ $I -lt 6 ]; do
$X mousemove $GRAB $ROW
$X keydown ctrl click 4 keyup ctrl
sleep 0.3
I=$((I + 1))
done
sleep 1
# Берём клип и тащим к правому краю, где и держим: рука стоит, едет
# только прокрутка.
$X mousemove $GRAB $ROW mousedown 1
sleep 0.3
$X mousemove 800 $ROW
sleep 0.3
$X mousemove $EDGE $ROW
sleep 6
$X mouseup 1
sleep 2
MOVED=$(start)
echo " начало клипа после перетаскивания с удержанием: $MOVED с"
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 -c "
moved = $MOVED
# После шести щелчков колесом в окне видно около полутора секунд монтажа.
# Указатель доезжает до кромки и встаёт, поэтому без прокрутки клип
# уезжает на эти полторы секунды и больше никуда. С прокруткой за шесть
# секунд удержания набегает втрое больше. Порог стоит между измеренными
# величинами: 1.1 с на сборке без прокрутки, 3+ с с прокруткой.
if moved < 2.5:
print(' ПЛОХО: клип уехал всего на %.3f с — монтаж не прокручивался' % moved)
raise SystemExit(1)
print(' ok: клип уехал на %.3f с, дальше видимой части' % moved)
"

117
tests/genquiet.sh Executable file
View file

@ -0,0 +1,117 @@
#!/bin/sh
# Тяжёлая сборка начинается не сразу, а когда правка закончилась.
#
# Наезд двух клипов на одной дорожке — это заказ на переход, и считается
# он минутами. Пока человек подбирает место, заказ меняется каждым
# движением руки, и начатая работа уходит в корзину. Поэтому список
# кусков пересобирается сразу (монтаж обязан показывать правду), а вот
# ffmpeg запускается, когда монтаж постоял тихо.
#
# Проверяется так: клип тащится на соседа, создавая наезд. С момента,
# когда кнопку отпустили, считается время до первой команды ffmpeg в
# журнале. Оно обязано быть близко к заданной тишине, но работа всё-таки
# обязана начаться — отложить не значит забыть.
#
# Журнал только читается: усечь его значит снести запись чужой работы.
#
# Запуск (экран :99 поднят):
# tests/genquiet.sh [<путь к nlve>]
set -e
cd "$(dirname "$0")/.."
BIN=${1:-$PWD/nlve}
DISP=${NLVE_DISPLAY:-:99}
SRC=$PWD/test/поток/пара.nlve
PROJ=$PWD/test/поток/тишина.nlve
LOG=$(dirname "$BIN")/nlve.log
[ -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; }
cp "$SRC" "$PROJ"
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 -rf "$PROJ-cache"
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 3
ROW=${NLVE_ROW:-505}
# Сколько команд ffmpeg в журнале сейчас. Считаем строки, а не чистим
# файл: журнал общий, и усечение уносит чужую работу.
runs() { grep -c '^> .*ffmpeg' "$LOG" 2>/dev/null || echo 0; }
BEFORE=$(runs)
echo " команд в журнале до правки: $BEFORE"
# Тащим первый клип вправо на соседа: наезд примерно в секунду.
$X mousemove 650 $ROW mousedown 1
sleep 0.3
$X mousemove 670 $ROW
sleep 0.3
$X mousemove 683 $ROW
sleep 0.3
$X mouseup 1
# С этой точки отсчёт. Опрашиваем журнал почаще, чтобы поймать момент
# запуска, а не ждать наугад.
T0=$(date +%s.%N)
WAITED=0
while [ "$WAITED" -lt 60 ]; do
N=$(runs)
[ "$N" -gt "$BEFORE" ] && break
sleep 0.2
WAITED=$((WAITED + 1))
done
T1=$(date +%s.%N)
AFTER=$(runs)
for p in $(pgrep -x nlve); do
tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -qx "DISPLAY=$DISP" && kill $p
done
pkill -f "тишина.nlve-cache" 2>/dev/null || true
rm -f "$PROJ.autosave"
python3 -c "
задержка = $T1 - $T0
было, стало = $BEFORE, $AFTER
print(' команд после правки: %d, пауза до запуска: %.2f с' % (стало, задержка))
if стало <= было:
print(' ПЛОХО: сборка не началась вовсе — отложить не значит забыть')
raise SystemExit(1)
# Задано полторы секунды тишины. Нижняя граница с запасом на разброс
# опроса, верхняя — чтобы отложенное не превратилось в забытое.
if задержка < 1.2:
print(' ПЛОХО: сборка началась через %.2f с — правку не дождались'
% задержка)
raise SystemExit(1)
if задержка > 6:
print(' ПЛОХО: сборка началась только через %.2f с' % задержка)
raise SystemExit(1)
print(' ok: сборка дождалась конца правки и всё-таки началась')
"

158
tests/insertat.sh Executable file
View file

@ -0,0 +1,158 @@
#!/bin/sh
# Файл из списка встаёт на курсор, а не в конец дорожки.
#
# Раньше клип падал в самый конец, стоило месту под ним оказаться
# занятым, — а оно занято почти всегда: клип длиной в целый файл упирается
# хвостом в то, что правее, даже когда начало легло в свободное место.
# Потом человек тащил его на нужное время и получал наезд, то есть заказ
# на сборку перехода.
#
# Проверяется так: курсор ставится посреди первого клипа, файл из списка
# добавляется на ту же дорожку. Клип обязан встать на курсор, монтаж —
# раздвинуться ровно на его длину, наездов и дыр появиться не должно.
# Всё читается из сохранённого проекта, а не с картинки.
#
# Запуск (экран :99 поднят):
# tests/insertat.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; }
# Невидимый экран поднят через xvfb-run, у него свой файл доступа, а в
# окружении обычно лежит файл рабочего экрана. С чужим файлом xdotool не
# видит ни одного окна, и проверка «проходит» вслепую, ничего не проверив.
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; }
cp "$SRC" "$PROJ"
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"
# Окно ищем по имени монтажа и проверяем, что оно на экране: по одному
# «nlve» находятся и обломки прошлых запусков, нажатия в них уходят в
# никуда, и проверка молча показывает «ничего не произошло».
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
$X mousemove 700 466 click 1 # курсор воспроизведения — посреди первого клипа
sleep 1
$X mousemove 200 512 click 1 # выбрать файл в списке проекта
sleep 1
# Меню списка: «Добавить на дорожку» — первый пункт, в подменю первой
# стоит V1, она же и нужна.
$X click 3
sleep 1.5
$X key Down; sleep 0.5
$X key Right; sleep 0.5
$X key Return; sleep 2
$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 - "$SRC" "$PROJ" <<'PY'
import json, sys
def clips(path, track):
d = json.load(open(path))
for t in d['tracks']:
if t['name'] == track:
return sorted(t['clips'], key=lambda c: c['tl_start'])
return []
было = clips(sys.argv[1], 'V1')
стало = clips(sys.argv[2], 'V1')
звук = clips(sys.argv[2], 'A1')
плохо = 0
медиа = {c['media'] for c in было}
новые = [c for c in стало if c['media'] not in медиа]
if len(новые) != 1:
print(' ПЛОХО: вставленный клип не найден (новых клипов %d)' % len(новые))
raise SystemExit(1)
новый = новые[0]
конец_было = max(c['tl_start'] + c['duration'] for c in было)
print(' вставленный клип: старт %.3f с, длина %.3f с' %
(новый['tl_start'], новый['duration']))
if новый['tl_start'] > конец_было - 0.001:
print(' ПЛОХО: клип упал в конец дорожки (%.3f с), а не на курсор'
% новый['tl_start'])
плохо = 1
elif новый['tl_start'] < 0.5:
print(' ПЛОХО: клип встал в начало, курсор был посреди первого клипа')
плохо = 1
else:
print(' ok: клип встал на курсор, внутри материала')
# Соседи отъехали ровно на длину вставки: монтаж раздвинулся, а не
# затёрся и не разъехался.
сдвиг = новый['duration']
последний_было = было[-1]
последний_стало = стало[-1]
ожидалось = последний_было['tl_start'] + сдвиг
if abs(последний_стало['tl_start'] - ожидалось) > 0.001:
print(' ПЛОХО: последний клип на %.3f с, ожидалось %.3f с'
% (последний_стало['tl_start'], ожидалось))
плохо = 1
else:
print(' ok: монтаж раздвинут ровно на длину вставки (%.3f с)' % сдвиг)
# Наезд на одной дорожке — это заказ на переход. Его тут быть не должно.
for имя, список in (('V1', стало), ('A1', звук)):
for a, b in zip(список, список[1:]):
зазор = b['tl_start'] - (a['tl_start'] + a['duration'])
if зазор < -0.001:
print(' ПЛОХО: наезд на %s в %.3f с (%.3f с)'
% (имя, b['tl_start'], -зазор))
плохо = 1
elif зазор > 0.001:
print(' ПЛОХО: дыра на %s в %.3f с (%.3f с)'
% (имя, a['tl_start'] + a['duration'], зазор))
плохо = 1
if not плохо:
print(' ok: ни наездов, ни дыр')
# Звук обязан уехать вместе с картинкой, иначе синхрон рассыплется.
if [round(c['tl_start'], 3) for c in звук] != [round(c['tl_start'], 3) for c in стало]:
print(' ПЛОХО: звук разошёлся с картинкой')
print(' картинка: %s' % ['%.3f' % c['tl_start'] for c in стало])
print(' звук: %s' % ['%.3f' % c['tl_start'] for c in звук])
плохо = 1
else:
print(' ok: звук уехал вместе с картинкой')
raise SystemExit(плохо)
PY

View file

@ -637,6 +637,166 @@ end;
Раньше курсор оставался там, где был, а рез мог попасть на долю кадра.
Теперь рез кладётся на кадровую сетку, а курсор встаёт ровно на место
реза — с ним и работают дальше. }
{ Раздвижка монтажа под вставку.
Клип из списка файлов встаёт туда, где стоит курсор, а не в конец
дорожки. Место под ним освобождается: клип, накрывающий точку вставки,
режется, и всё от этой точки правее едет вправо на длину нового — на
всех дорожках сразу, иначе звук отстанет от картинки. }
procedure CheckMakeRoom;
var
Proj: TProject;
Info: TMediaInfo;
Media: TMediaItem;
Timeline: TTimelineControl;
Moved, I: Integer;
Gaps, Overlaps: Integer;
Step, At, Shift: Double;
Track: TTrack;
begin
WriteLn;
WriteLn('Раздвижка монтажа под вставку');
WriteLn;
Info := TestInfo;
Info.FpsNum := 60000;
Info.FpsDen := 1001;
Proj := TProject.Create;
Timeline := TTimelineControl.Create(nil);
try
Proj.Clear;
Proj.AddTrack('V1');
Proj.AddTrackOfKind('A1', tkAudio);
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
{ Три клипа встык на картинке и столько же на звуке: 0..10, 10..20,
20..30. }
for I := 0 to 2 do
begin
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, I * 10);
Proj.AddClip(Proj.Tracks[1], Media, 0, 10, I * 10);
end;
Timeline.Project := Proj;
{ Вставляем пять секунд посреди второго клипа.
Ожидаемое считается по той же кадровой сетке, а не круглыми
числами: при 60000/1001 ни 15, ни 5 на кадр не попадают, и точка
реза с величиной сдвига садятся на ближайшие кадры. }
Step := Info.FpsDen / Info.FpsNum;
At := Round(15 / Step) * Step;
Shift := Round(5 / Step) * Step;
Moved := Timeline.MakeRoomAt(15, 5);
CheckTrue('что-то отодвинуто: ' + IntToStr(Moved), Moved >= 4);
CheckTrue('второй клип разошёлся надвое: клипов стало ' +
IntToStr(Proj.Tracks[0].Count), Proj.Tracks[0].Count = 4);
CheckTrue('звук разошёлся так же', Proj.Tracks[1].Count = 4);
{ Голова второго клипа осталась на месте, хвост уехал ровно на пять. }
CheckTrue('голова на месте: ' + SecondsToStr(Proj.Tracks[0][1].TlStart),
Abs(Proj.Tracks[0][1].TlStart - 10) < 0.001);
CheckTrue('хвост отъехал на длину вставки: ' +
SecondsToStr(Proj.Tracks[0][2].TlStart),
Abs(Proj.Tracks[0][2].TlStart - (At + Shift)) < 0.001);
CheckTrue('третий клип отъехал на столько же: ' +
SecondsToStr(Proj.Tracks[0][3].TlStart),
Abs(Proj.Tracks[0][3].TlStart - (20 + Shift)) < 0.001);
CheckTrue('звук уехал вместе с картинкой',
Abs(Proj.Tracks[1][2].TlStart - (At + Shift)) < 0.001);
CheckTrue('освобождено ровно столько, сколько просили: ' +
SecondsToStr(Proj.Tracks[0][2].TlStart - Proj.Tracks[0][1].TlEnd),
Abs((Proj.Tracks[0][2].TlStart - Proj.Tracks[0][1].TlEnd) - Shift)
< 0.001);
{ В освобождённом месте никого нет, и больше нигде не появилось ни
дыр, ни наездов. }
CheckTrue('место освобождено', Proj.Tracks[0].ClipAt(At + 1) = nil);
Gaps := 0;
Overlaps := 0;
for I := 0 to Proj.TrackCount - 1 do
begin
Track := Proj.Tracks[I];
for Moved := 0 to Track.Count - 2 do
if Track[Moved].TlEnd > Track[Moved + 1].TlStart + 0.001 then
Inc(Overlaps)
else if (Track[Moved].TlEnd < Track[Moved + 1].TlStart - 0.001) and
(Abs(Track[Moved].TlEnd - At) > 0.001) then
Inc(Gaps);
end;
CheckTrue('наездов не появилось', Overlaps = 0);
CheckTrue('лишних дыр не появилось', Gaps = 0);
{ Вставка в свободное место за концом монтажа ничего не двигает. }
Moved := Timeline.MakeRoomAt(100, 5);
CheckTrue('за концом монтажа двигать нечего', Moved = 0);
finally
Timeline.Free;
Proj.Free;
end;
end;
{ Клип притягивается к границе и хвостом, а не только головой.
Подводят кусок к началу соседа слева: голова встаёт в свободное место,
а хвост при этом накрывает соседа. Наезд на одной дорожке — это заказ
на переход, то есть минуты сборки из-за промаха на полпикселя. }
procedure CheckSnapTail;
var
Proj: TProject;
Info: TMediaInfo;
Media: TMediaItem;
Timeline: TTimelineControl;
Mover: TClip;
Got: Double;
begin
WriteLn;
WriteLn('Прилипание хвостом');
WriteLn;
Info := TestInfo;
Info.FpsNum := 60000;
Info.FpsDen := 1001;
Proj := TProject.Create;
Timeline := TTimelineControl.Create(nil);
try
Proj.Clear;
Proj.AddTrack('V1');
Media := Proj.AddMedia('/несуществующий/видео.mp4', Info);
Proj.Master := Info;
{ Сосед занимает 20..30, двигаемый длиной 4 стоит пока в сторонке. }
Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 20);
Mover := Proj.AddClip(Proj.Tracks[0], Media, 0, 4, 50);
Timeline.Project := Proj;
Timeline.PixelsPerSecond := 10; { допуск прилипания — 1.2 с }
{ Голова метит в 15.7, хвост при этом пришёлся бы на 19.7 — почти
встык с соседом. Правильный ответ: 16, тогда хвост ровно на 20. }
Got := Timeline.SnapTime(15.7, Mover, Mover.Duration);
CheckTrue('хвост сел на начало соседа: ' + SecondsToStr(Got),
Abs(Got - 16) < 0.001);
{ Голова у границы по-прежнему липнет головой: там ближе она. }
Got := Timeline.SnapTime(19.8, Mover, Mover.Duration);
CheckTrue('голова села на начало соседа: ' + SecondsToStr(Got),
Abs(Got - 20) < 0.001);
{ Без длины поведение прежнее: хвост не рассматривается. }
Got := Timeline.SnapTime(15.7, Mover);
CheckTrue('без длины притяжения хвостом нет: ' + SecondsToStr(Got),
Abs(Got - 15.7) < 0.001);
{ Хвост липнет и к концу соседа: клип встаёт вплотную справа. }
Got := Timeline.SnapTime(25.8, Mover, Mover.Duration);
CheckTrue('хвост сел на конец соседа: ' + SecondsToStr(Got),
Abs(Got - 26) < 0.001);
finally
Timeline.Free;
Proj.Free;
end;
end;
procedure CheckSplitAndPlayhead;
var
Proj: TProject;
@ -994,6 +1154,8 @@ begin
CheckRubberBand;
CheckFrameGridRepair;
CheckSplitAndPlayhead;
CheckMakeRoom;
CheckSnapTail;
CheckSplitAllTracks;
CheckGapTolerance;
CheckSourceRect;