From 0967678b618e414913cd27458ed3b9602ed611ab Mon Sep 17 00:00:00 2001 From: 4aiman Date: Mon, 31 Aug 2026 12:01:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D0=BA=D1=81=D1=82=20=D0=B2=D0=B6?= =?UTF-8?q?=D0=B8=D0=B3=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B8=20=D0=B2=20?= =?UTF-8?q?=D1=84=D0=BE=D1=82=D0=BE=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D1=8E;?= =?UTF-8?q?=20=D0=B4=D0=BE=D1=80=D0=BE=D0=B6=D0=BA=D0=B8=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D1=81=D0=B0=20=D1=83=D1=87=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вжигание не доходило до сгенерированных вставок. Картинки и чёрное отбирались первой же веткой сборки — до проверки на пересборку — и копировались целиком вместе с отметкой о разметке. Галка вжигания стояла, кусок в журнале был помечен «с текстом», а в ролике надписи не было. Причину показал журнал сборки рядом с файлом: помечено «пересобирается», а команда — копирование. Теперь вставка с разметкой идёт общей веткой пересборки, и время у неё берётся своё: она начинается с нуля, а не с середины монтажа. У линейки появился верхний этаж в двенадцать точек — про отметки. Протяжка по нему отмечает участок, даже если отметок не было; щелчок без движения их снимает. Уголки по краям тянутся, как и раньше. Заголовок дорожки: правый щелчок открывает меню — поднять, опустить, удалить (с клипами — с подтверждением). Заголовок можно и потащить: порядок дорожек есть порядок перекрытия, и менять его перетаскиванием привычнее, чем через меню. --- README.md | 15 ++- .../2026-08-31_участок-качество-пропажи.md | 27 ++++ src/uExport.pas | 27 +++- src/uMainForm.pas | 119 ++++++++++++++++++ src/uTimelineCtrl.pas | 98 ++++++++++++++- 5 files changed, 276 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f9e6ba9..167891f 100644 --- a/README.md +++ b/README.md @@ -153,14 +153,23 @@ xdg-mime query default application/x-nlve-project времени. На слабой машине это может быть заметнее, потому выключатель и есть. -**Отмеченный участок.** Клавиша `I` ставит начало по курсору, `O` — -конец; оба пункта есть в меню «Правка», там же снятие отметок. Участок -показан полосой под линейкой и стойками по краям. Пока он отмечен, сборка +**Отмеченный участок.** У линейки два этажа. Верхний, в двенадцать точек, +— про отметки: протяжка по нему отмечает участок, даже если отметок не +было, а щелчок без движения их снимает. По краям участка стоят жёлтые +уголки, за них тянут. Нижний этаж — про курсор, как и раньше. + +Клавиши тоже остались: `I` ставит начало по курсору, `O` — конец; оба +пункта есть в меню «Правка», там же снятие отметок. Пока он отмечен, сборка собирает только его — это единственный способ проверить правку на длинном монтаже, не тратя на каждую проверку столько же времени, сколько идёт сам ролик. Отметки живут в файле проекта: участок относится к монтажу, а не к привычкам человека. +**Дорожки.** Правый щелчок по заголовку открывает меню: поднять, +опустить, удалить. Заголовок можно и просто потащить мышью — порядок +дорожек и есть порядок перекрытия, и менять его перетаскиванием +привычнее. Удаление дорожки с клипами спрашивает подтверждение. + **Схлопывание.** Кнопка внизу показывает и режим, и состояние: утоплена — автосхлопывание включено, и тогда при перетаскивании клипа всё, что начинается не раньше него, едет следом. Стрелка рядом открывает выбор diff --git a/docs/session-logs/2026-08-31_участок-качество-пропажи.md b/docs/session-logs/2026-08-31_участок-качество-пропажи.md index 7196fcb..d738b85 100644 --- a/docs/session-logs/2026-08-31_участок-качество-пропажи.md +++ b/docs/session-logs/2026-08-31_участок-качество-пропажи.md @@ -62,3 +62,30 @@ попытка вжечь текст до этого места ничего не дала: повторное уплощение стёрло отметки о разметке. Вжигание перенесено на окончательный список — так же, как в окне сборки. + +## Третий заход: текст не вжигался в фотографию + +Пользователь согласился перенести клипы на правильные дорожки, поставил +галку вжигания — и текста в ролике снова не оказалось. Журнал сборки, +заведённый как раз для таких случаев, показал причину за минуту: + + кусок 2: img_4ec7c38d3285_15000.mkv, из 0.000000, длина 5.000000, + с текстом, пересобирается + > ffmpeg ... -i img_4ec7c38d3285_15000.mkv -map 0:v:0 -c copy ... + +Кусок помечен «с текстом» и «пересобирается», а команда — копирование. +Причина: сгенерированные вставки (картинки, чёрное) отбирались первой же +веткой сборки, до проверки на пересборку, и копировались целиком. Текст +до них просто не доходил. + +Теперь вставка с разметкой идёт общей веткой пересборки, и время у неё +берётся своё — она начинается с нуля, а не с середины монтажа. Проверено +сборкой: на кадре с фотографией 3748 белых точек в нижней полосе, сама +фотография на месте. + +## Что ещё сделано в этом заходе + +- Верхний этаж линейки: протяжка отмечает участок, даже если отметок не + было; щелчок без движения их снимает. +- Заголовок дорожки: правый щелчок открывает меню (поднять, опустить, + удалить), перетаскивание меняет порядок дорожек. diff --git a/src/uExport.pas b/src/uExport.pas index c639865..b9b558e 100644 --- a/src/uExport.pas +++ b/src/uExport.pas @@ -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, diff --git a/src/uMainForm.pas b/src/uMainForm.pas index 72b857c..b82c17a 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -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; diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas index 26aff0b..425f48a 100644 --- a/src/uTimelineCtrl.pas +++ b/src/uTimelineCtrl.pas @@ -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;