322 lines
12 KiB
C#
322 lines
12 KiB
C#
#nullable enable
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using T3.Core.Settings;
|
|
using T3.Editor.Gui.UiHelpers;
|
|
using T3.Editor.UiModel.ProjectHandling;
|
|
|
|
namespace T3.Editor.Gui.Windows.RenderExport;
|
|
|
|
internal static partial class RenderPaths
|
|
{
|
|
private static readonly Regex _matchFileVersionPattern = FileVersionPatternRegex();
|
|
|
|
public static string ResolveProjectRelativePath(string path)
|
|
{
|
|
var project = ProjectView.Focused?.OpenedProject;
|
|
if (project != null && path.StartsWith('.'))
|
|
{
|
|
return Path.Combine(project.Package.Folder, path);
|
|
}
|
|
|
|
// TODO: Make project directory selection smarter
|
|
return path.StartsWith('.')
|
|
? Path.Combine(UserSettings.Config.ProjectDirectories[0], FileLocations.RenderSubFolder, path)
|
|
: path;
|
|
}
|
|
|
|
public static string GetTargetFilePath(RenderSettings.RenderModes mode)
|
|
{
|
|
var settings = RenderSettings.Current;
|
|
if (mode == RenderSettings.RenderModes.Video)
|
|
{
|
|
var targetPath = ResolveProjectRelativePath(RenderSettings.Current.VideoFilePath ?? string.Empty);
|
|
if (settings.AutoIncrementVersionNumber)
|
|
{
|
|
if (!IsFilenameIncrementable(targetPath))
|
|
{
|
|
targetPath = GetNextIncrementedPath(targetPath);
|
|
}
|
|
|
|
// Keep numbering consistent across container formats: jump past the highest existing version among
|
|
// files sharing this name stem regardless of extension, so e.g. render-v08.mov isn't picked while
|
|
// render-v08.mp4 already exists.
|
|
var highestVersion = GetHighestExistingVersion(Path.GetDirectoryName(targetPath), Path.GetFileName(targetPath));
|
|
if (highestVersion >= 0)
|
|
{
|
|
targetPath = WithVersionNumber(targetPath, highestVersion + 1);
|
|
}
|
|
|
|
while (File.Exists(targetPath))
|
|
{
|
|
targetPath = GetNextIncrementedPath(targetPath);
|
|
}
|
|
}
|
|
return targetPath;
|
|
}
|
|
|
|
var folder = ResolveProjectRelativePath(RenderSettings.Current.SequenceFilePath ?? string.Empty);
|
|
var subFolder = RenderSettings.Current.SequenceFileName ?? "v01";
|
|
var prefix = RenderSettings.Current.SequencePrefix ?? "render";
|
|
|
|
if (settings.AutoIncrementSubFolder)
|
|
{
|
|
var targetToIncrement = settings.CreateSubFolder ? subFolder : prefix;
|
|
if (!IsFilenameIncrementable(targetToIncrement))
|
|
{
|
|
targetToIncrement = GetNextIncrementedPath(targetToIncrement);
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
var checkPath = settings.CreateSubFolder
|
|
? Path.Combine(folder, targetToIncrement, prefix)
|
|
: Path.Combine(folder, targetToIncrement);
|
|
|
|
if (FileExists(checkPath))
|
|
{
|
|
targetToIncrement = GetNextIncrementedPath(targetToIncrement);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (settings.CreateSubFolder) subFolder = targetToIncrement;
|
|
else prefix = targetToIncrement;
|
|
}
|
|
|
|
if (settings.CreateSubFolder)
|
|
{
|
|
return Path.Combine(folder, subFolder, prefix);
|
|
}
|
|
|
|
return Path.Combine(folder, prefix);
|
|
}
|
|
|
|
public static string GetExpectedTargetDisplayPath(RenderSettings.RenderModes mode)
|
|
{
|
|
var targetPath = GetTargetFilePath(mode);
|
|
var settings = RenderSettings.Current;
|
|
|
|
if (mode == RenderSettings.RenderModes.Video)
|
|
{
|
|
return targetPath;
|
|
}
|
|
|
|
// Image sequence path
|
|
var frameCount = RenderTiming.ComputeFrameCount(settings);
|
|
var frameRange = frameCount <= 1 ? "0000" : $"0000..{(frameCount - 1):D4}";
|
|
return $"{targetPath}_{frameRange}.{settings.FileFormat.ToString().ToLower()}";
|
|
}
|
|
|
|
public static bool FileExists(string targetPath)
|
|
{
|
|
if (RenderSettings.Current.RenderMode == RenderSettings.RenderModes.Video)
|
|
{
|
|
return File.Exists(targetPath);
|
|
}
|
|
|
|
// For image sequences, check if the first frame or the folder exists
|
|
if (RenderSettings.Current.CreateSubFolder)
|
|
{
|
|
var directory = Path.GetDirectoryName(targetPath);
|
|
if (directory != null && Directory.Exists(directory))
|
|
{
|
|
// If the directory exists, check if it contains any files or subdirectories
|
|
try
|
|
{
|
|
return Directory.EnumerateFileSystemEntries(directory).Any();
|
|
}
|
|
catch
|
|
{
|
|
return true; // Assume exists if we can't access
|
|
}
|
|
}
|
|
}
|
|
|
|
var firstFrame = $"{targetPath}_0000.{RenderSettings.Current.FileFormat.ToString().ToLower()}";
|
|
return File.Exists(firstFrame);
|
|
}
|
|
|
|
public static bool ValidateOrCreateTargetFolder(string targetFile)
|
|
{
|
|
var directory = Path.GetDirectoryName(targetFile);
|
|
if (directory == null || Directory.Exists(directory))
|
|
return true;
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Warning($"Failed to create target folder '{directory}': {e.Message}");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static string SanitizeFilename(string filename)
|
|
{
|
|
if (string.IsNullOrEmpty(filename))
|
|
return "output";
|
|
|
|
var invalidChars = Path.GetInvalidFileNameChars();
|
|
foreach (var c in invalidChars)
|
|
filename = filename.Replace(c.ToString(), "_");
|
|
|
|
return filename.Trim();
|
|
}
|
|
|
|
public static bool IsFilenameIncrementable(string? path = null)
|
|
{
|
|
var filename = Path.GetFileName(path ?? RenderSettings.Current.VideoFilePath);
|
|
return !string.IsNullOrEmpty(filename) && _matchFileVersionPattern.Match(filename).Success;
|
|
}
|
|
|
|
public static void TryIncrementVideoFileName()
|
|
{
|
|
var path = RenderSettings.Current.VideoFilePath;
|
|
if (string.IsNullOrEmpty(path) || !IsFilenameIncrementable(path))
|
|
return;
|
|
|
|
RenderSettings.Current.VideoFilePath = GetNextIncrementedPath(path);
|
|
}
|
|
|
|
public static string GetVersionString(string? path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return string.Empty;
|
|
|
|
var name = Path.GetFileName(path);
|
|
if (string.IsNullOrEmpty(name))
|
|
return string.Empty;
|
|
|
|
var match = FileVersionPatternRegex().Match(name);
|
|
if (match.Success)
|
|
{
|
|
return "v" + match.Groups[1].Value;
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
public static string GetNextVersionString(string? path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return "v01";
|
|
|
|
var filename = Path.GetFileName(path);
|
|
var match = FileVersionPatternRegex().Match(filename);
|
|
if (match.Success)
|
|
{
|
|
var versionGroup = match.Groups[1];
|
|
if (int.TryParse(versionGroup.Value, out var versionNumber))
|
|
{
|
|
var digits = Math.Clamp(versionGroup.Value.Length, 2, 4);
|
|
return "v" + (versionNumber + 1).ToString("D" + digits);
|
|
}
|
|
}
|
|
|
|
return "v01";
|
|
}
|
|
|
|
// Highest version number among sibling files that share this name's stem (the parts before and after the
|
|
// version), ignoring extension. Returns -1 when the folder holds no such file.
|
|
private static int GetHighestExistingVersion(string? directory, string fileNameWithVersion)
|
|
{
|
|
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
|
|
return -1;
|
|
|
|
var nameNoExt = Path.GetFileNameWithoutExtension(fileNameWithVersion);
|
|
var match = _matchFileVersionPattern.Match(nameNoExt);
|
|
if (!match.Success)
|
|
return -1;
|
|
|
|
var digits = match.Groups[1];
|
|
var prefix = nameNoExt[..digits.Index];
|
|
var suffix = nameNoExt[(digits.Index + digits.Length)..];
|
|
|
|
var highest = -1;
|
|
foreach (var file in Directory.EnumerateFiles(directory, prefix + "*")) // OS-side prefix filter keeps it cheap
|
|
{
|
|
var candidate = Path.GetFileNameWithoutExtension(file);
|
|
if (candidate.Length <= prefix.Length + suffix.Length
|
|
|| !candidate.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
|
|| !candidate.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var middle = candidate[prefix.Length..(candidate.Length - suffix.Length)];
|
|
if (middle.Length is >= 2 and <= 4 && int.TryParse(middle, out var version) && version > highest)
|
|
highest = version;
|
|
}
|
|
|
|
return highest;
|
|
}
|
|
|
|
// Replaces the version digits in path's filename with versionNumber, preserving the existing digit width (2-4).
|
|
private static string WithVersionNumber(string path, int versionNumber)
|
|
{
|
|
var filename = Path.GetFileName(path);
|
|
var match = _matchFileVersionPattern.Match(filename);
|
|
if (!match.Success)
|
|
return path;
|
|
|
|
var versionGroup = match.Groups[1];
|
|
var width = Math.Clamp(versionGroup.Value.Length, 2, 4);
|
|
var newFilename = filename.Remove(versionGroup.Index, versionGroup.Length)
|
|
.Insert(versionGroup.Index, versionNumber.ToString("D" + width));
|
|
|
|
var directory = Path.GetDirectoryName(path);
|
|
return directory == null ? newFilename : Path.Combine(directory, newFilename);
|
|
}
|
|
|
|
public static string GetNextIncrementedPath(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return "output";
|
|
|
|
var filename = Path.GetFileName(path);
|
|
var directory = Path.GetDirectoryName(path);
|
|
string newFilename;
|
|
|
|
var match = _matchFileVersionPattern.Match(filename);
|
|
if (!match.Success)
|
|
{
|
|
var extension = Path.GetExtension(filename);
|
|
var nameWithoutExtension = Path.GetFileNameWithoutExtension(filename);
|
|
newFilename = nameWithoutExtension + "_v01" + extension;
|
|
}
|
|
else
|
|
{
|
|
var versionGroup = match.Groups[1];
|
|
var versionString = versionGroup.Value;
|
|
|
|
if (!int.TryParse(versionString, out var versionNumber))
|
|
{
|
|
var extension = Path.GetExtension(filename);
|
|
var nameWithoutExtension = Path.GetFileNameWithoutExtension(filename);
|
|
newFilename = nameWithoutExtension + "_v01" + extension;
|
|
}
|
|
else
|
|
{
|
|
var digits = Math.Clamp(versionString.Length, 2, 4);
|
|
var newVersionNumberString = (versionNumber + 1).ToString("D" + digits);
|
|
|
|
// Replace only the version number part within the matched group
|
|
newFilename = filename.Remove(versionGroup.Index, versionGroup.Length)
|
|
.Insert(versionGroup.Index, newVersionNumberString);
|
|
}
|
|
}
|
|
|
|
return directory == null ? newFilename : Path.Combine(directory, newFilename);
|
|
}
|
|
|
|
[GeneratedRegex(@"(?:^|[\s_\-.])v(\d{2,4})(?=$|[\s_\-.])", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
|
private static partial Regex FileVersionPatternRegex();
|
|
} |