From 34767fd71104524781de1155d782beceb5fd65f2 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Mon, 31 Aug 2026 01:08:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B7=20=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B5=20=D0=BD=D0=B5=20=D1=82=D0=B5=D1=80=D1=8F=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BA=D0=BB=D0=B8=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После разреза хвост оставался без цветового ключа: перенос настроек был написан перечислением полей, а ключ появился позже и в перечень не попал. Хвост переставал быть наложением, становился обычным победителем и закрывал собой всё, что ниже — вместо компаса в углу окно показывало пурпурную плашку во весь кадр. Перечисление заменено на CopySettingsFrom: одно место, где сказано, что переносится при копировании клипа, а что делает клип этим клипом. Иначе та же ловушка досталась бы следующему свойству. Проверка в lfmcheck: рез клипа с ключом, прозрачностью, громкостью и кадрированием — всё остаётся у обеих половин, связки у них разные и непустые. Проекты, разрезанные прежней сборкой, правка задним числом не чинит: у хвоста ключ надо включить заново. --- .../2026-08-31_наложение-в-предпросмотре.md | 24 +++++ src/uProject.pas | 28 ++++++ src/uTimelineCtrl.pas | 14 ++- tests/lfmcheck/lfmcheck.lpr | 97 ++++++++++++++++++- 4 files changed, 154 insertions(+), 9 deletions(-) diff --git a/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md b/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md index c24bd3d..30e4dcb 100644 --- a/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md +++ b/docs/session-logs/2026-08-31_наложение-в-предпросмотре.md @@ -91,3 +91,27 @@ mpv 0.37, видео 2688x1512 HEVC 87 Мбит/с, 600 кадров (10 с ма из-за которой монтаж не помещался в окно). Файл не пострадал — проверено по содержимому, — но теперь тестовые скрипты задают `XDG_CONFIG_HOME`, `XDG_CACHE_HOME` и `XDG_DATA_HOME` своими. + +## Рез терял цветовой ключ + +Показано пользователем на снимках: до реза оверлей лежал компасом в углу, +после реза (S) в окне остался только он сам, во весь кадр и с пурпуром. + +Причина: разрез создаёт хвост и переносит на него настройки по одной — +громкость, прозрачность, кадрирование, затухания. Цветового ключа в этом +списке не было, потому что он появился позже. Хвост без ключа перестаёт +быть наложением, становится обычным победителем и закрывает собой всё, +что ниже. + +Чинить перечислением ещё одного поля значило бы оставить ту же ловушку +следующему свойству. Поэтому у клипа появился `CopySettingsFrom`: одно +место, где сказано, что переносится при копировании, а что делает клип +этим клипом (имя, место, кусок исходника, связка, выделение). + +Проверка добавлена в `lfmcheck`: рез клипа с ключом, прозрачностью, +громкостью и кадрированием — всё это должно оказаться у обеих половин, а +связки у них должны быть разные и непустые. + +Важное для уже сделанных монтажей: правка не чинит проекты задним +числом. Если оверлей был разрезан старой сборкой, у хвоста ключ надо +включить заново. diff --git a/src/uProject.pas b/src/uProject.pas index 26ddd5e..ef9bebb 100644 --- a/src/uProject.pas +++ b/src/uProject.pas @@ -143,6 +143,19 @@ type FColorKey: TColorKey; public constructor Create; + { Переносит на этот клип всё, что настроено у другого: громкость, + кадрирование, прозрачность, ключ, затухания, текст. + + Не переносится то, что делает клип этим клипом: имя, место на + монтаже, кусок исходника, связка и выделение. + + Зачем метод, а не переписывание полей на месте: копия клипа нужна в + нескольких местах (рез — самое частое), и каждое новое свойство + иначе приходится вспоминать в каждом из них. Один раз не вспомнили — + и после реза половина клипа теряла цветовой ключ, а вместе с ним и + прозрачность: она переставала быть наложением и закрывала собой всё, + что ниже. } + procedure CopySettingsFrom(ASource: TClip); function TlEnd: Double; function SrcOut: Double; { Попадает ли момент внутрь клипа. Правый край не включается: там уже @@ -402,6 +415,21 @@ end; { --- TClip --- } +procedure TClip.CopySettingsFrom(ASource: TClip); +begin + if ASource = nil then + Exit; + FKind := ASource.Kind; + FVolume := ASource.Volume; + FTransform := ASource.Transform; + FOpacity := ASource.Opacity; + FColorKey := ASource.ColorKey; + FFadeIn := ASource.FadeIn; + FFadeOut := ASource.FadeOut; + FText := ASource.Text; + FTextStyle := ASource.TextStyle; +end; + constructor TClip.Create; begin inherited Create; diff --git a/src/uTimelineCtrl.pas b/src/uTimelineCtrl.pas index 6f2a01a..753eeeb 100644 --- a/src/uTimelineCtrl.pas +++ b/src/uTimelineCtrl.pas @@ -2232,19 +2232,17 @@ begin Tail := TClip.Create; Tail.Id := Clip.Id + '_b'; Tail.MediaId := Clip.MediaId; - Tail.Kind := Clip.Kind; Tail.SrcIn := Clip.SrcIn + Offset; Tail.TlStart := FPlayhead; Tail.Duration := Clip.Duration - Offset; Tail.EdgeOut := Clip.EdgeOut; - { Хвост — тот же клип, только вторая его половина. Всё, что было - настроено, должно перейти к нему: иначе после реза половина клипа - теряет громкость и кадрирование. } - Tail.Volume := Clip.Volume; - Tail.Opacity := Clip.Opacity; + { Хвост — тот же клип, только вторая его половина. Всё настроенное + переходит к нему одним махом: перечислять поля по одному значит + однажды забыть новое. } + Tail.CopySettingsFrom(Clip); + { Кроме появления: оно относится к началу клипа, а начало осталось у + первой половины. } Tail.FadeIn := 0; - Tail.FadeOut := Clip.FadeOut; - Tail.Transform := Clip.Transform; { Связка хвоста — новая. diff --git a/tests/lfmcheck/lfmcheck.lpr b/tests/lfmcheck/lfmcheck.lpr index b01418f..31ce56f 100644 --- a/tests/lfmcheck/lfmcheck.lpr +++ b/tests/lfmcheck/lfmcheck.lpr @@ -15,7 +15,7 @@ uses { Interfaces нужен, чтобы связалась вся LCL и заработала загрузка файлов форм. } Interfaces, Forms, Classes, SysUtils, Math, TypInfo, uTheme, - uMediaProbe, uProject, uTimelineCtrl, + uMediaProbe, uProject, uTransform, uTimelineCtrl, uPanCropForm, uDepsForm, uMismatchForm, uProgressForm, uProjectInfoForm, uRemuxForm; @@ -153,6 +153,100 @@ begin end; end; +{ Рез не должен терять настройки клипа. + + Половина клипа после реза — тот же клип, и цветовой ключ, кадрирование, + прозрачность, громкость и текст должны остаться при обеих половинах. + Однажды не остались: у хвоста пропадал ключ, клип переставал быть + наложением и закрывал собой всё, что под ним. } +procedure CheckSplitKeepsSettings; +var + Proj: TProject; + Info: TMediaInfo; + Media: TMediaItem; + Timeline: TTimelineControl; + Head, Tail, Cl: TClip; + Key: TColorKey; + Tr: TMediaTransform; + I: Integer; +begin + WriteLn; + WriteLn('Рез сохраняет настройки клипа'); + WriteLn; + + Info := Default(TMediaInfo); + Info.Valid := True; + Info.Kind := mkVideo; + Info.Duration := 600; + Info.HasVideo := True; + Info.VideoCodec := 'hevc'; + Info.Width := 1920; + Info.Height := 1080; + Info.FpsNum := 30; + Info.FpsDen := 1; + Info.PixFmt := 'yuv420p'; + + Proj := TProject.Create; + Timeline := TTimelineControl.Create(nil); + try + Proj.Clear; + Proj.AddTrack('V1'); + Media := Proj.AddMedia('/несуществующий/видео.mp4', Info); + Cl := Proj.AddClip(Proj.Tracks[0], Media, 0, 10, 0); + + Key := DefaultColorKey; + Key.Enabled := True; + Key.Similarity := 22; + Cl.ColorKey := Key; + Cl.Opacity := 0.5; + Cl.Volume := 0.3; + Tr := Cl.Transform; + Tr.Zoom := 0.4; + Tr.OffsetX := 0.7; + Cl.Transform := Tr; + Cl.GroupId := 'g1'; + + Timeline.Project := Proj; + Timeline.PixelsPerSecond := 100; + Timeline.Playhead := 4; + Timeline.SplitAtPlayhead(False, False); + + CheckTrue('клипов стало два', Proj.Tracks[0].Count = 2); + if Proj.Tracks[0].Count <> 2 then + Exit; + + Head := nil; + Tail := nil; + for I := 0 to Proj.Tracks[0].Count - 1 do + if Proj.Tracks[0][I].TlStart < 0.001 then + Head := Proj.Tracks[0][I] + else + Tail := Proj.Tracks[0][I]; + CheckTrue('обе половины нашлись', (Head <> nil) and (Tail <> nil)); + if (Head = nil) or (Tail = nil) then + Exit; + + CheckTrue('ключ остался у хвоста', Tail.ColorKey.Enabled); + CheckTrue('и с тем же цветом', Tail.ColorKey.Color = Key.Color); + CheckTrue('и с тем же допуском', Tail.ColorKey.Similarity = 22); + CheckTrue('прозрачность осталась', Abs(Tail.Opacity - 0.5) < 0.001); + CheckTrue('громкость осталась', Abs(Tail.Volume - 0.3) < 0.001); + CheckTrue('кадрирование осталось', + (Abs(Tail.Transform.Zoom - 0.4) < 0.001) and + (Abs(Tail.Transform.OffsetX - 0.7) < 0.001)); + CheckTrue('ключ остался и у головы', Head.ColorKey.Enabled); + { Связки разные, и обе не пустые: иначе удаление одной половины + уносило бы вторую. } + CheckTrue('связка у половин разная и не пустая', + (Head.GroupId <> Tail.GroupId) and (Head.GroupId <> '') and + (Tail.GroupId <> '')); + CheckTrue('появление у хвоста снято', Abs(Tail.FadeIn) < 0.001); + finally + Timeline.Free; + Proj.Free; + end; +end; + begin WriteLn('Свойства из файлов форм'); WriteLn; @@ -180,6 +274,7 @@ begin CheckForm(TRemuxForm); CheckPlayheadSnap; + CheckSplitKeepsSettings; WriteLn; if Failed = 0 then