Files
2026-07-13 13:13:17 +08:00

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();
}