Крест перехода там, где клипы не накладываются

На монтаже рисовался косой крест через целый клип, строка состояния
обещала переход, а наложения не было: ролик собирался верно. Крест уходил
сам после Ctrl+Z, после любого действия с отменой и после схлопывания.

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

Отмена и схлопывание лечили ровно потому, что пересортировывают списки.
Сборка всё это время была права: она идёт по границам и сортирует
дорожку сама.

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

Проверка tests/dragorder.sh тащит связку за звук с конца монтажа в
середину и читает порядок из сохранённого файла. На прежней сборке
дорожка картинки остаётся в порядке 0, 14, 7.991 — проверка падает.
This commit is contained in:
4aiman 2026-10-16 14:01:51 +03:00
commit 95d92a7582
4 changed files with 230 additions and 8 deletions

View file

@ -537,7 +537,17 @@ Linux потом искала по нему `.dll`.
дорожку, величину перекрытия и сами клипы. Переход заказывают мышью, а
промахнуться легко — на общем плане одна точка экрана это секунды, — и
раньше он появлялся сюрпризом: человек искал, почему сборка опять не
кончается.
кончается. Меряется наезд той же мерой, что и в сборке: короче половины
кадра — не наезд, и говорить о нём незачем.
Косой крест наезда и это сообщение берут соседей из списка клипов
дорожки, а он отсортирован по времени. Держать этот порядок обязан
каждый, кто клипы двигает: перетаскивание ведёт за собой связку на
звуковой дорожке и соседей при схлопывании, и все их дорожки
пересортировываются (`SortMovedTracks`). Пока сортировалась только та
дорожка, с которой тащили, монтаж рисовал крест через целый клип там,
где ничего не накладывалось, и обещал переход, которого никто не
заказывал.
Сама сборка при этом не начинается с первым же движением. Список кусков
пересобирается сразу, а ffmpeg запускается, когда монтаж постоял тихо
@ -1010,7 +1020,7 @@ Wine с новой библиотекой не грузилось ничего.
уход выглядит как накапливающийся рассинхрон, которого нет. На это уже
попались один раз.
Ещё семь проверок гоняют саму программу на невидимом экране
Ещё девять проверок гоняют саму программу на невидимом экране
(`Xvfb :99`). Рабочий экран они не занимают:
```
@ -1021,9 +1031,11 @@ Wine с новой библиотекой не грузилось ничего.
./tests/insertat.sh [<путь к nlve>] файл из списка встаёт на курсор
./tests/dragscroll.sh [<путь к nlve>] монтаж едет следом за клипом у кромки
./tests/genquiet.sh [<путь к nlve>] тяжёлая сборка ждёт конца правки
./tests/dropgap.sh [<путь к nlve>] клип садится в дыру без наезда
./tests/dragorder.sh [<путь к nlve>] связка переехала — порядок цел
```
Последним пяти нужен монтаж в `test/поток/`: `длинный-фейд.nlve` — один
Последним семи нужен монтаж в `test/поток/`: `длинный-фейд.nlve` — один
клип с затуханием секунд на семьдесят (такой кусок сводится около
полутора минут, и всё это время человек продолжает работать),
`пара.nlve` — два клипа без затуханий. Путь к первому задаётся
@ -1040,10 +1052,12 @@ xdotool не видит ни одного окна и проверка «про
экран шире: `Xvfb :98 -screen 0 1600x1000x24` и `ЭКРАН=:98`.
Каждую из них стоит прогонять и на прежней сборке: проверка, которая
проходит везде, не проверяет ничего. Все семь на сборке до своей правки
падают. Для трёх последних замерено: клип у кромки уезжает на 0.7 с
против 4.2 с, файл из списка падает в конец дорожки вместо курсора,
сборка начинается через 0.2 с вместо 1.6 с.
проходит везде, не проверяет ничего. Все девять на сборке до своей правки
падают. Замерено: клип у кромки уезжает на 0.7 с против 4.2 с, файл из
списка падает в конец дорожки вместо курсора, сборка начинается через
0.2 с вместо 1.6 с, клип в дыру садится с наездом 0.007383 с, а дорожка
картинки после переноса связки остаётся в порядке 0, 14, 7.991 — не по
времени.
## Структура

View file

@ -3876,6 +3876,10 @@ begin
begin
A := Track[C];
B := Track[C + 1];
{ Соседи по списку обязаны быть соседями и по времени. Сбитый
порядок — не наезд, и обещать по нему переход нельзя. }
if B.TlStart < A.TlStart then
Continue;
{ Мера та же, какой меряет сборка (`uFlatten`): кусок короче
половины кадра она пропускает, значит и перехода из него не
будет. Говорить о наезде, которого никто не станет собирать,

View file

@ -159,6 +159,11 @@ type
FOnSelectionModeChange: TNotifyEvent;
FGroupClips: array of TClip;
FGroupOrigStarts: array of Double;
{ Дорожки, чьи клипы поехали вместе с перетаскиваемым: связка ведёт
звук за картинкой, схлопывание ведёт соседей, и лежат они на чужих
дорожках. Список нужен, чтобы после каждого сдвига вернуть им
порядок по времени. }
FMovedTracks: array of TTrack;
FOnPlayheadChange: TNotifyEvent;
FOnStructureChange: TNotifyEvent;
@ -397,6 +402,10 @@ type
{ Клипы одной связки с перетаскиваемым и их исходные позиции. }
procedure CollectGroupClips(AClip: TClip);
procedure ApplyGroupMove(const ADelta: Double);
{ Запомнить дорожку как тронутую перетаскиванием. }
procedure NoteMovedTrack(ATrack: TTrack);
{ Вернуть порядок по времени всем тронутым дорожкам. }
procedure SortMovedTracks;
{ Сдвинуть клипы влево, закрыв дыру под курсором. }
{ True — дыра нашлась и схлопнута. False — схлопывать было нечего:
окно скажет об этом, чтобы команда не выглядела сломанной. }
@ -1382,6 +1391,12 @@ begin
begin
A := ATrack[I];
B := ATrack[I + 1];
{ Соседи по списку обязаны быть соседями и по времени. Если порядок
всё-таки сбит, это не наложение, а перепутанный список: мерять по
нему нечего. Страховка от креста через весь клип там, где ничего не
накладывается. }
if B.TlStart < A.TlStart then
Continue;
if B.TlStart >= A.TlEnd - 0.0005 then
Continue;
L := TimeToX(B.TlStart);
@ -2682,6 +2697,9 @@ var
begin
FGroupClips := nil;
FGroupOrigStarts := nil;
{ Набор тронутых дорожек начинается здесь: это первый сбор при нажатии,
соседи по схлопыванию добавятся следом. }
FMovedTracks := nil;
if (FProject = nil) or (AClip = nil) or (AClip.GroupId = '') then
Exit;
@ -2696,10 +2714,48 @@ begin
SetLength(FGroupOrigStarts, N + 1);
FGroupClips[N] := Clip;
FGroupOrigStarts[N] := Clip.TlStart;
NoteMovedTrack(FProject.Tracks[T]);
Inc(N);
end;
end;
procedure TTimelineControl.NoteMovedTrack(ATrack: TTrack);
var
I: Integer;
begin
if ATrack = nil then
Exit;
for I := 0 to High(FMovedTracks) do
if FMovedTracks[I] = ATrack then
Exit;
SetLength(FMovedTracks, Length(FMovedTracks) + 1);
FMovedTracks[High(FMovedTracks)] := ATrack;
end;
{ Порядок клипов в списке дорожки — по времени начала, и держать его
обязан каждый, кто клипы двигает.
Перетаскивание двигает не одну дорожку: связка ведёт звук за картинкой,
схлопывание ведёт соседей, а в режиме «все дорожки» — сразу на
нескольких. Пока сортировалась только та дорожка, с которой тащили,
клип, уехавший с конца монтажа в середину, оставался в списке
последним. Соседями по списку становились клипы, не соседние во
времени, и монтаж рисовал косой крест перехода через целый клип, а
строка состояния обещала переход, которого никто не заказывал.
Наложения при этом не было вовсе: сборка идёт по границам и сортирует
дорожку сама, поэтому ролик получался верный. Лечилось любой отменой и
схлопыванием — ровно тем, что пересортировывает списки. }
procedure TTimelineControl.SortMovedTracks;
var
I: Integer;
begin
if FDragTrack <> nil then
FDragTrack.Sort;
for I := 0 to High(FMovedTracks) do
FMovedTracks[I].Sort;
end;
procedure TTimelineControl.ApplyGroupMove(const ADelta: Double);
var
I: Integer;
@ -2737,6 +2793,7 @@ begin
SetLength(FRippleOrigStarts, N + 1);
FRippleClips[N] := Clip;
FRippleOrigStarts[N] := Clip.TlStart;
NoteMovedTrack(Track);
Inc(N);
end;
end;
@ -2828,7 +2885,9 @@ begin
end;
end;
FDragTrack.Sort;
{ Порядок возвращаем всем, кто поехал, а не только своей дорожке:
связка и соседи лежат на чужих. }
SortMovedTracks;
Invalidate;
end;
end;

145
tests/dragorder.sh Executable file
View file

@ -0,0 +1,145 @@
#!/bin/sh
# После переноса связки клипы в списке дорожки идут по времени.
#
# Список клипов дорожки обязан быть отсортирован по началу. Двигает
# перетаскивание не одну дорожку: связка ведёт звук за картинкой,
# схлопывание ведёт соседей. Сортировалась же только та дорожка, с
# которой тащили, и клип, уехавший с конца монтажа в середину, оставался
# в списке последним. Соседями по списку становились клипы, не соседние
# во времени, и монтаж рисовал косой крест перехода через целый клип, а
# строка состояния обещала переход, которого никто не заказывал.
#
# Наложения при этом не было: сборка идёт по границам и сортирует
# дорожку сама, поэтому ролик выходил верный, а крест уходил сам после
# любой отмены или схлопывания — ровно от того, что списки
# пересортировывались.
#
# Тащим за ЗВУК: тогда картинка едет связкой, то есть тем самым путём,
# который порядок и ломал. Порядок читается из сохранённого проекта:
# файл пишет клипы в порядке списка и сам их не сортирует.
#
# Запуск (экран :99 поднят):
# tests/dragorder.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
V['clips'] = [клип(ов, 'a_v', 'ga', 0.0, 4.0, 100.0),
клип(ов, 'b_v', 'gb', 14.0, 6.0, 200.0),
клип(ов, 'd_v', 'gd', 30.0, 3.5, 300.0)]
A['clips'] = [клип(оа, 'a_a', 'ga', 0.0, 4.0, 100.0),
клип(оа, 'b_a', 'gb', 14.0, 6.0, 200.0),
клип(оа, 'd_a', 'gd', 30.0, 3.5, 300.0)]
json.dump(d, open(sys.argv[2], 'w'), ensure_ascii=False)
print(' связки: 0..4, 14..20 и 30..33.5 — последнюю тащим в промежуток')
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
AROW=${NLVE_AROW:-705} # высота звуковой дорожки A1 в окне
# Два щелчка колесом от себя: в окно должен попасть весь монтаж.
$X mousemove 700 "$AROW"
$X keydown ctrl click 5 keyup ctrl; sleep 0.4
$X keydown ctrl click 5 keyup ctrl; sleep 1
# Берём последнюю связку за звук и тащим в промежуток между второй и
# третьей.
$X mousemove 1190 "$AROW" mousedown 1; sleep 0.4
$X mousemove 1000 "$AROW"; sleep 0.3
$X mousemove 850 "$AROW"; sleep 0.3
$X mousemove 761 "$AROW"; 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
двинулось = False
for t in d['tracks']:
кл = t['clips']
if len(кл) < 3:
continue
порядок = [c['tl_start'] for c in кл]
print(' %s: %s' % (t['name'], ' '.join('%.3f' % s for s in порядок)))
# Клип, уехавший с конца в середину, обязан занять своё место и в
# списке, а не остаться в нём последним.
if порядок != sorted(порядок):
print(' ПЛОХО: на %s клипы в списке не по времени' % t['name'])
плохо = 1
# Клип уехал в пустой промежуток, значит наезда быть не должно:
# иначе проверка мерила бы заодно и настоящий переход.
по_времени = sorted(кл, key=lambda c: c['tl_start'])
for a, b in zip(по_времени, по_времени[1:]):
наезд = (a['tl_start'] + a['duration']) - b['tl_start']
if наезд > 0.5 * 1001 / 60000:
print(' ПЛОХО: на %s наезд %.6f с — целились в пустое место'
% (t['name'], наезд))
плохо = 1
for c in кл:
if c['id'].startswith('d_') and c['tl_start'] < 13:
двинулось = True
if not двинулось:
print(' ПЛОХО: клип никуда не уехал — проверка ничего не значит')
плохо = 1
if плохо:
raise SystemExit(1)
print(' ok: связка переехала, и обе дорожки остались в порядке')
PY