Текст вжигается и в фотографию; дорожки и полоса участка

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

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

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

Заголовок дорожки: правый щелчок открывает меню — поднять, опустить,
удалить (с клипами — с подтверждением). Заголовок можно и потащить:
порядок дорожек есть порядок перекрытия, и менять его перетаскиванием
привычнее, чем через меню.
This commit is contained in:
4aiman 2026-08-31 12:01:12 +03:00
commit 59c778d2f6
4 changed files with 249 additions and 10 deletions

View file

@ -153,14 +153,23 @@ xdg-mime query default application/x-nlve-project
времени. На слабой машине это может быть заметнее, потому выключатель и
есть.
**Отмеченный участок.** Клавиша `I` ставит начало по курсору, `O` —
конец; оба пункта есть в меню «Правка», там же снятие отметок. Участок
показан полосой под линейкой и стойками по краям. Пока он отмечен, сборка
**Отмеченный участок.** У линейки два этажа. Верхний, в двенадцать точек,
— про отметки: протяжка по нему отмечает участок, даже если отметок не
было, а щелчок без движения их снимает. По краям участка стоят жёлтые
уголки, за них тянут. Нижний этаж — про курсор, как и раньше.
Клавиши тоже остались: `I` ставит начало по курсору, `O` — конец; оба
пункта есть в меню «Правка», там же снятие отметок. Пока он отмечен, сборка
собирает только его — это единственный способ проверить правку на длинном
монтаже, не тратя на каждую проверку столько же времени, сколько идёт сам
ролик. Отметки живут в файле проекта: участок относится к монтажу, а не к
привычкам человека.
**Дорожки.** Правый щелчок по заголовку открывает меню: поднять,
опустить, удалить. Заголовок можно и просто потащить мышью — порядок
дорожек и есть порядок перекрытия, и менять его перетаскиванием
привычнее. Удаление дорожки с клипами спрашивает подтверждение.
**Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена —
автосхлопывание включено, и тогда при перетаскивании клипа всё, что
начинается не раньше него, едет следом. Стрелка рядом открывает выбор

View file

@ -533,7 +533,7 @@ begin
нужного: так одна и та же чёрная вставка годится для дыр любой
длины. Значит вырезать надо и из них, но притягивать к опорному
кадру нечего — они начинаются с нуля, а ноль всегда опорный. }
if FSegments[I].Kind <> skVideo then
if (FSegments[I].Kind <> skVideo) and (FSegments[I].SubFile = '') then
begin
Dst := FCacheDir + Format('part_%.3d_a%s', [I, FIntermediateExt]);
Rep.Index := I;
@ -587,10 +587,17 @@ begin
Continue;
end;
{ Кусок от файла, не совпадающего с параметрами проекта. Скопировать
его нельзя — пересобираем. Но только его: соседние куски, снятые тем
же оборудованием, что и весь проект, по-прежнему копируются. }
if FSegments[I].Transcode then
{ Кусок от файла, не совпадающего с параметрами проекта, или с
вжигаемым текстом. Скопировать его нельзя — пересобираем. Но только
его: соседние куски, снятые тем же оборудованием, что и весь
проект, по-прежнему копируются.
Сгенерированные вставки — картинки и чёрное — сюда попадают только
с текстом: без него их незачем перекодировать, они и так в
параметрах проекта. Раньше они уходили в ветку выше и копировались
вместе с текстом мимо: галка вжигания стояла, а надписи на
фотографии не появлялось. }
if FSegments[I].Transcode or (FSegments[I].SubFile <> '') then
begin
SetStage(Format('Пересобираю кусок %d из %d (файл не совпадает с проектом)',
[I + 1, Length(FSegments)]));
@ -604,8 +611,16 @@ begin
Rep.Encoded := True;
FStepWork := FSegments[I].Duration;
{ У сгенерированной вставки время своё: она начинается с нуля, и
просить у неё кусок с середины монтажа бессмысленно. }
if FSegments[I].Kind <> skVideo then
Rep.RequestedStart := 0
else
Rep.RequestedStart := FSegments[I].SrcStart;
Rep.ActualStart := Rep.RequestedStart;
if not RunFfmpeg(ArgsTranscodeSegment(FSegments[I].FilePath, Dst,
FSegments[I].SrcStart, FSegments[I].SrcStart + FSegments[I].Duration,
Rep.RequestedStart, Rep.RequestedStart + FSegments[I].Duration,
FMaster.Width, FMaster.Height, FMaster.FpsNum, FMaster.FpsDen,
FMaster.VideoCodec, FMaster.PixFmt, FMaster.AudioCodec,
FMaster.SampleRate, FMaster.Channels, FSegments[I].Volume,

View file

@ -182,6 +182,10 @@ type
FPoolMenu: TPopupMenu;
FPoolAddMenu: TMenuItem;
FRippleMenu: TPopupMenu;
{ Меню дорожки по правому щелчку в её заголовке. }
FTrackMenu: TPopupMenu;
{ Дорожка, для которой открыто меню. }
FTrackMenuIndex: Integer;
{ Пункт «Недавние проекты» в меню «Файл». Его содержимое собирается
заново при каждом изменении списка. }
FRecentMenu: TMenuItem;
@ -204,6 +208,13 @@ type
procedure UpdateSubtitles;
procedure UpdateRippleButton;
procedure CheckMisplacedClips;
procedure BuildTrackMenu;
procedure TimelineTrackMenu(Sender: TObject; ATrackIndex: Integer;
const AScreenPos: TPoint);
procedure TimelineRegionChange(Sender: TObject);
procedure TrackMenuDelete(Sender: TObject);
procedure TrackMenuUp(Sender: TObject);
procedure TrackMenuDown(Sender: TObject);
procedure UpdateSnapButton;
procedure UpdateGlueButton;
procedure UpdateCenterButton;
@ -437,6 +448,7 @@ begin
lstPool.DragMode := dmAutomatic;
BuildPoolMenu;
BuildTrackMenu;
RefreshRecentMenu;
AllowDropFiles := True;
OnDropFiles := @FormFilesDropped;
@ -577,6 +589,8 @@ begin
FTimeline.OnMediaDrop := @TimelineMediaDrop;
FTimeline.OnClipTransform := @TimelineClipTransform;
FTimeline.OnTrackAdded := @TimelineTrackAdded;
FTimeline.OnTrackMenu := @TimelineTrackMenu;
FTimeline.OnRegionChange := @TimelineRegionChange;
FTimeline.OnActiveTrackChange := @TimelineActiveTrackChange;
end;
@ -2448,6 +2462,111 @@ begin
FRippleMenu.PopUp(P.X, P.Y);
end;
{ --- Меню дорожки --- }
{ Меню по правому щелчку в заголовке дорожки.
Раньше дорожку можно было только добавить и убрать последнюю, а убрать
чаще нужно не последнюю. Порядок дорожек тоже правится здесь — тем, кому
перетаскивание заголовка неудобно. }
{ Участок подвинули мышью: показываем, что вышло, и помечаем проект
изменённым — отметки в нём и хранятся. }
procedure TMainForm.TimelineRegionChange(Sender: TObject);
begin
FProject.Modified := True;
UpdateCaption;
if FProject.HasRegion then
SetStatus(Format('Участок: %s — %s',
[SecondsToStr(FProject.RegionStart), SecondsToStr(FProject.RegionEnd)]))
else
SetStatus('Отметки участка сняты: собирается весь монтаж.');
end;
procedure TMainForm.BuildTrackMenu;
var
Item: TMenuItem;
begin
FTrackMenu := TPopupMenu.Create(Self);
Item := TMenuItem.Create(FTrackMenu);
Item.Caption := 'Поднять дорожку';
Item.OnClick := @TrackMenuUp;
FTrackMenu.Items.Add(Item);
Item := TMenuItem.Create(FTrackMenu);
Item.Caption := 'Опустить дорожку';
Item.OnClick := @TrackMenuDown;
FTrackMenu.Items.Add(Item);
Item := TMenuItem.Create(FTrackMenu);
Item.Caption := '-';
FTrackMenu.Items.Add(Item);
Item := TMenuItem.Create(FTrackMenu);
Item.Caption := 'Удалить дорожку';
Item.OnClick := @TrackMenuDelete;
FTrackMenu.Items.Add(Item);
end;
procedure TMainForm.TimelineTrackMenu(Sender: TObject; ATrackIndex: Integer;
const AScreenPos: TPoint);
begin
if (ATrackIndex < 0) or (ATrackIndex >= FProject.TrackCount) then
Exit;
FTrackMenuIndex := ATrackIndex;
FTrackMenu.Items[0].Enabled := ATrackIndex > 0;
FTrackMenu.Items[1].Enabled := ATrackIndex < FProject.TrackCount - 1;
FTrackMenu.PopUp(AScreenPos.X, AScreenPos.Y);
end;
procedure TMainForm.TrackMenuUp(Sender: TObject);
begin
if FTrackMenuIndex <= 0 then
Exit;
FProject.BeginChange;
FProject.MoveTrack(FTrackMenuIndex, FTrackMenuIndex - 1);
TimelineStructureChange(nil);
SetStatus('Дорожка поднята.');
end;
procedure TMainForm.TrackMenuDown(Sender: TObject);
begin
if (FTrackMenuIndex < 0) or (FTrackMenuIndex >= FProject.TrackCount - 1) then
Exit;
FProject.BeginChange;
FProject.MoveTrack(FTrackMenuIndex, FTrackMenuIndex + 1);
TimelineStructureChange(nil);
SetStatus('Дорожка опущена.');
end;
{ Удаление дорожки. С клипами — только с подтверждением: вместе с
дорожкой уходит всё, что на ней лежит, а отменить можно только одним
шагом назад. }
procedure TMainForm.TrackMenuDelete(Sender: TObject);
var
Track: TTrack;
{ Не Name: так называется свойство самой формы, и локальная переменная
его перекроет — компилятор ловит, но сообщение неочевидное. }
TrackName: string;
begin
if (FTrackMenuIndex < 0) or (FTrackMenuIndex >= FProject.TrackCount) then
Exit;
Track := FProject.Tracks[FTrackMenuIndex];
TrackName := Track.Name;
if (Track.Count > 0) and (ThemedMessageDlg('nlve',
Format('На дорожке %s %d %s. Удалить её вместе с ними?',
[TrackName, Track.Count,
Plural(Track.Count, 'клип', 'клипа', 'клипов')]),
mtConfirmation, [mbYes, mbNo], 0) <> mrYes) then
Exit;
FProject.BeginChange;
FProject.RemoveTrack(FTrackMenuIndex);
TimelineStructureChange(nil);
SetStatus('Дорожка ' + TrackName + ' удалена.');
end;
{ --- Меню библиотеки --- }
procedure TMainForm.BuildPoolMenu;

View file

@ -40,12 +40,18 @@ type
taFadeIn, // тянем появление за верхний левый угол
taFadeOut, // тянем уход за верхний правый угол
taRegionIn, // тянем начало отмеченного участка
taRegionOut // тянем конец отмеченного участка
taRegionOut, // тянем конец отмеченного участка
taDragTrack // тянем дорожку за заголовок, меняя порядок
);
{ Что находится под указателем в полосе заголовков дорожек. }
THeaderPart = (hpNone, hpBody, hpMute, hpSolo);
{ Правый щелчок по заголовку дорожки. Само меню строит окно: компонент
не знает, что с дорожкой можно делать. }
TTrackMenuEvent = procedure(Sender: TObject; ATrackIndex: Integer;
const AScreenPos: TPoint) of object;
{ Файл из списка проекта бросили на монтаж.
ATrackIndex = -1 означает, что бросили ниже всех дорожек и нужна новая.
@ -125,6 +131,11 @@ type
FOnSelectionChange: TNotifyEvent;
{ Отмеченный участок подвинули мышью. }
FOnRegionChange: TNotifyEvent;
{ Правый щелчок по заголовку дорожки: окно покажет своё меню. }
FOnTrackMenu: TTrackMenuEvent;
{ Дорожка, которую тащат за заголовок. }
FDragTrackIndex: Integer;
FDragTrackFrom: Integer;
FOnMediaDrop: TMediaDropEvent;
FOnClipTransform: TClipEvent;
FOnTrackAdded: TNotifyEvent;
@ -294,6 +305,8 @@ type
write FOnSelectionChange;
property OnRegionChange: TNotifyEvent read FOnRegionChange
write FOnRegionChange;
property OnTrackMenu: TTrackMenuEvent read FOnTrackMenu
write FOnTrackMenu;
property OnMediaDrop: TMediaDropEvent read FOnMediaDrop write FOnMediaDrop;
{ Нажата кнопка кадрирования в углу клипа. }
property OnClipTransform: TClipEvent read FOnClipTransform
@ -314,6 +327,11 @@ const
SnapPixels = 8;
{ Уголок отметки участка: сторона треугольника в точках. }
RegionHandleWidth = 9;
{ Полоса участка вверху линейки: в ней живут уголки, и протяжка по ней
отмечает участок, а не двигает курсор. Так же устроено в монтажных
программах: у линейки два этажа, верхний про отметки, нижний про
курсор. }
RegionBarHeight = 12;
{ Сдвиг мыши, до которого нажатие считается щелчком, а не перетаскиванием.
Совсем без запаса нельзя: рука дрожит, и попасть кнопкой в один и тот же
@ -405,6 +423,8 @@ begin
FActiveTrack := 0;
FSnapEnabled := True;
FPlayheadSnap := True;
FDragTrackIndex := -1;
FDragTrackFrom := -1;
end;
destructor TTimelineControl.Destroy;
@ -1738,6 +1758,21 @@ begin
Exit;
end;
{ Правый щелчок по заголовку — меню дорожки. Удалять и переставлять
дорожки больше негде: в главном меню есть только «добавить» и «убрать
последнюю», а убрать нужно бывает не последнюю. }
if (Button = mbRight) and (X < FHeaderWidth) and (Y >= FRulerHeight) then
begin
Idx := TrackIndexAt(Y);
if (Idx >= 0) and Assigned(FOnTrackMenu) then
begin
FActiveTrack := Idx;
Invalidate;
FOnTrackMenu(Self, Idx, ClientToScreen(Point(X, Y)));
end;
Exit;
end;
if Button <> mbLeft then
Exit;
@ -1763,6 +1798,11 @@ begin
hpBody:
begin
FActiveTrack := Idx;
{ Заголовок можно потащить: порядок дорожек — это приоритет, и
менять его перетаскиванием привычнее, чем через меню. }
FAction := taDragTrack;
FDragTrackIndex := Idx;
FDragTrackFrom := Idx;
Invalidate;
if Assigned(FOnActiveTrackChange) then
FOnActiveTrackChange(Self);
@ -1781,6 +1821,18 @@ begin
1: begin FAction := taRegionIn; Exit; end;
2: begin FAction := taRegionOut; Exit; end;
end;
{ Верхний этаж линейки — про участок: протяжка по нему отмечает его
заново, даже если отметок не было. Нижний — про курсор. }
if (Y < RegionBarHeight) and (FProject <> nil) then
begin
FAction := taRegionOut;
FProject.RegionStart := Max(0, SnapPlayhead(XToTime(X)));
FProject.RegionEnd := FProject.RegionStart;
Invalidate;
Exit;
end;
FAction := taScrub;
Playhead := Max(0, SnapPlayhead(XToTime(X)));
Exit;
@ -1963,6 +2015,19 @@ begin
на стык, соскакивал бы с него от малейшего движения мыши. }
Playhead := Max(0, SnapPlayhead(XToTime(X)));
taDragTrack:
if (FProject <> nil) and (FDragTrackIndex >= 0) then
begin
Idx := TrackIndexAt(Y);
if (Idx >= 0) and (Idx <> FDragTrackIndex) then
begin
FProject.MoveTrack(FDragTrackIndex, Idx);
FDragTrackIndex := Idx;
FActiveTrack := Idx;
Invalidate;
end;
end;
taRegionIn:
DragRegionEdge(X, True);
@ -2247,6 +2312,37 @@ begin
{ Shift читается и здесь: курсор по щелчку ставится при отпускании, и
решение о прилипании должно быть по клавише, которую держат сейчас. }
FSnapSuspended := ssShift in Shift;
{ Дорожку переставили — предпросмотр надо пересобрать: порядок дорожек
и есть порядок перекрытия. Сообщаем один раз, в конце: во время
перетаскивания дорожка проходит через все промежуточные места. }
if FAction = taDragTrack then
begin
FAction := taNone;
if (FProject <> nil) and (FDragTrackIndex <> FDragTrackFrom) then
begin
FProject.Modified := True;
StructureChanged;
end;
FDragTrackIndex := -1;
Exit;
end;
{ Отметка участка протяжкой: щелчок без движения снимает отметки — это
и есть привычный способ от них избавиться. }
if (FAction in [taRegionIn, taRegionOut]) and (FProject <> nil) then
begin
FAction := taNone;
if not FProject.HasRegion then
begin
FProject.RegionStart := 0;
FProject.RegionEnd := 0;
end;
if Assigned(FOnRegionChange) then
FOnRegionChange(Self);
Invalidate;
Exit;
end;
WasEditing := FAction in [taDragClip, taTrimIn, taTrimOut, taVolume,
taFadeIn, taFadeOut];
WasTrimIn := FAction = taTrimIn;