diff --git a/CUE4Parse b/CUE4Parse index d807df72..4c333298 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit d807df729bda91706598ef2f4531bd9799dfb8ef +Subproject commit 4c3332989787d0b57325d160137e9fa61d82a139 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index 31da19b9..b37f3b8b 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -11,7 +11,9 @@ using CUE4Parse; using FModel.Framework; using FModel.Services; using FModel.Settings; +using FModel.Views.Snooper; using Newtonsoft.Json; +using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; using MessageBox = AdonisUI.Controls.MessageBox; using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; @@ -118,10 +120,12 @@ public partial class App .WriteTo.Console(outputTemplate: template, theme: AnsiConsoleTheme.Literate) .WriteTo.File(outputTemplate: template, path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.log")) + .MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance) #else .Enrich.With() .WriteTo.File(outputTemplate: template, path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.log")) + .MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance) #endif .CreateLogger(); diff --git a/FModel/Extensions/LogEventExtensions.cs b/FModel/Extensions/LogEventExtensions.cs new file mode 100644 index 00000000..620d6798 --- /dev/null +++ b/FModel/Extensions/LogEventExtensions.cs @@ -0,0 +1,11 @@ +using Serilog.Events; + +namespace FModel.Extensions; + +public static class LogEventExtensions +{ + public static string GetContext(this LogEvent log, string propertyName) + { + return log.Properties.TryGetValue(propertyName, out var value) ? value.ToString().Trim('"') : string.Empty; + } +} diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 861a11fe..784f33ee 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -121,6 +121,9 @@ + + + @@ -149,6 +152,9 @@ + + + diff --git a/FModel/Framework/ImGuiController.cs b/FModel/Framework/ImGuiController.cs index b62fde21..0768de97 100644 --- a/FModel/Framework/ImGuiController.cs +++ b/FModel/Framework/ImGuiController.cs @@ -66,18 +66,69 @@ public class ImGuiController : IDisposable var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini")); io.NativePtr->IniFilename = (byte*)iniFileNamePtr; } - - // If not found, Fallback to default ImGui Font - var normalPath = @"C:\Windows\Fonts\segoeui.ttf"; - var boldPath = @"C:\Windows\Fonts\segoeuib.ttf"; - var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf"; - if (File.Exists(normalPath)) - FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale); - if (File.Exists(boldPath)) - FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale); - if (File.Exists(semiBoldPath)) - FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale); + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + var assemblyName = assembly.GetName().Name; + byte[] LoadFont(string name) + { + using var stream = assembly.GetManifestResourceStream($"{assemblyName}.Resources.{name}") + ?? throw new FileNotFoundException($"Embedded font '{name}' not found."); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + + var faSolid = LoadFont("fa-solid-900.otf"); + var faRegular = LoadFont("fa-regular-400.otf"); + var faBrands = LoadFont("fa-brands-400.otf"); + + unsafe + { + // FA5 icons live in E000–F8FF (brands/regular/solid) + var iconRanges = stackalloc ushort[] { 0xe000, 0xf8ff, 0 }; + + var cfg = ImGuiNative.ImFontConfig_ImFontConfig(); + cfg->MergeMode = 1; + cfg->PixelSnapH = 1; + cfg->GlyphMinAdvanceX = 16f; + // FontDataOwnedByAtlas = 0 because we manage the GCHandle lifetime ourselves + cfg->FontDataOwnedByAtlas = 0; + + void MergeFontAwesome(byte[] solid, byte[] regular, byte[] brands) + { + fixed (byte* pSolid = solid) + fixed (byte* pRegular = regular) + fixed (byte* pBrands = brands) + { + io.Fonts.AddFontFromMemoryTTF((IntPtr)pSolid, solid.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges); + io.Fonts.AddFontFromMemoryTTF((IntPtr)pRegular, regular.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges); + io.Fonts.AddFontFromMemoryTTF((IntPtr)pBrands, brands.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges); + } + } + + // If not found, Fallback to default ImGui Font + var normalPath = @"C:\Windows\Fonts\segoeui.ttf"; + var boldPath = @"C:\Windows\Fonts\segoeuib.ttf"; + var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf"; + + if (File.Exists(normalPath)) + { + FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale); + MergeFontAwesome(faSolid, faRegular, faBrands); + } + if (File.Exists(boldPath)) + { + FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale); + MergeFontAwesome(faSolid, faRegular, faBrands); + } + if (File.Exists(semiBoldPath)) + { + FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale); + MergeFontAwesome(faSolid, faRegular, faBrands); + } + + ImGuiNative.ImFontConfig_destroy(cfg); + } io.Fonts.AddFontDefault(); io.Fonts.Build(); // Build font atlas diff --git a/FModel/Resources/fa-brands-400.otf b/FModel/Resources/fa-brands-400.otf new file mode 100644 index 00000000..9e013323 Binary files /dev/null and b/FModel/Resources/fa-brands-400.otf differ diff --git a/FModel/Resources/fa-regular-400.otf b/FModel/Resources/fa-regular-400.otf new file mode 100644 index 00000000..7cf3bee7 Binary files /dev/null and b/FModel/Resources/fa-regular-400.otf differ diff --git a/FModel/Resources/fa-solid-900.otf b/FModel/Resources/fa-solid-900.otf new file mode 100644 index 00000000..b00559cc Binary files /dev/null and b/FModel/Resources/fa-solid-900.otf differ diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 42991de9..28aeda90 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -4,13 +4,9 @@ using System.IO; using System.Windows; using System.Windows.Input; using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.Nanite; using CUE4Parse.UE4.Versions; -using CUE4Parse_Conversion; -using CUE4Parse_Conversion.Animations; -using CUE4Parse_Conversion.Meshes; -using CUE4Parse_Conversion.Textures; -using CUE4Parse_Conversion.UEFormat.Enums; +using CUE4Parse_Conversion.Options; +using CUE4Parse_Conversion.Writers.UEFormat.Enums; using CUE4Parse.UE4.Lua.unluac; using FModel.Framework; using FModel.ViewModels; @@ -57,26 +53,23 @@ namespace FModel.Settings return endpoint.Overwrite || endpoint.IsValid; } - [JsonIgnore] - public ExporterOptions ExportOptions => new() + public static ExportOptions GetExportOptions() { - LodFormat = Default.LodExportFormat, - MeshFormat = Default.MeshExportFormat, - NaniteMeshFormat = Default.NaniteMeshExportFormat, - AnimFormat = Default.MeshExportFormat switch - { - EMeshFormat.UEFormat => EAnimFormat.UEFormat, - _ => EAnimFormat.ActorX - }, - MaterialFormat = Default.MaterialExportFormat, - TextureFormat = Default.TextureExportFormat, - SocketFormat = Default.SocketExportFormat, - CompressionFormat = Default.CompressionFormat, - Platform = Default.CurrentDir.TexturePlatform, - ExportMorphTargets = Default.SaveMorphTargets, - ExportMaterials = Default.SaveEmbeddedMaterials, - ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr - }; + return new ExportOptions( + Default.MeshExportFormat, + Default.NaniteMeshExportFormat, + Default.MeshQuality, + Default.CurrentDir.TexturePlatform, + Default.TextureExportFormat, + 100, + Default.SaveHdrTexturesAsHdr, + Default.MaterialExportFormat, + Default.SaveEmbeddedMaterials, + Default.SaveMorphTargets, + Default.SocketExportFormat, + Default.CompressionFormat + ); + } private bool _showChangelog = true; public bool ShowChangelog @@ -422,15 +415,22 @@ namespace FModel.Settings set => SetProperty(ref _meshExportFormat, value); } - private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.OnlyNaniteLOD; + private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.NaniteOnly; public ENaniteMeshFormat NaniteMeshExportFormat { get => _naniteMeshExportFormat; set => SetProperty(ref _naniteMeshExportFormat, value); } - private EMaterialFormat _materialExportFormat = EMaterialFormat.FirstLayer; - public EMaterialFormat MaterialExportFormat + private EMeshQuality _meshQuality = EMeshQuality.Highest; + public EMeshQuality MeshQuality + { + get => _meshQuality; + set => SetProperty(ref _meshQuality, value); + } + + private EMaterialDepth _materialExportFormat = EMaterialDepth.TopLayerOnly; + public EMaterialDepth MaterialExportFormat { get => _materialExportFormat; set => SetProperty(ref _materialExportFormat, value); @@ -457,13 +457,6 @@ namespace FModel.Settings set => SetProperty(ref _compressionFormat, value); } - private ELodFormat _lodExportFormat = ELodFormat.FirstLod; - public ELodFormat LodExportFormat - { - get => _lodExportFormat; - set => SetProperty(ref _lodExportFormat, value); - } - private bool _showSkybox = true; public bool ShowSkybox { diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 840afb83..b354c70a 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -1678,32 +1678,34 @@ public class CUE4ParseViewModel : ViewModel private void SaveExport(UObject export, bool updateUi = true) { - var toSave = new Exporter(export, UserSettings.Default.ExportOptions); - var toSaveDirectory = new DirectoryInfo(UserSettings.Default.ModelDirectory); - if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath)) - { - Interlocked.Increment(ref ExportedCount); - Log.Information("Successfully saved {FilePath}", savedFilePath); - if (updateUi) - { - FLogger.Append(ELog.Information, () => - { - FLogger.Text("Successfully saved ", Constants.WHITE); - FLogger.Link(label, savedFilePath, true); - }); - } - } - else - { - Interlocked.Increment(ref FailedExportCount); - Log.Error("{FileName} could not be saved", export.Name); - FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true)); - } + // TODO: export session + // var toSave = new Exporter(export, UserSettings.Default.ExportOptions); + // var toSaveDirectory = new DirectoryInfo(UserSettings.Default.ModelDirectory); + // if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath)) + // { + // Interlocked.Increment(ref ExportedCount); + // Log.Information("Successfully saved {FilePath}", savedFilePath); + // if (updateUi) + // { + // FLogger.Append(ELog.Information, () => + // { + // FLogger.Text("Successfully saved ", Constants.WHITE); + // FLogger.Link(label, savedFilePath, true); + // }); + // } + // } + // else + // { + // Interlocked.Increment(ref FailedExportCount); + // Log.Error("{FileName} could not be saved", export.Name); + // FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true)); + // } } private readonly object _rawData = new (); public void ExportData(GameFile entry, bool updateUi = true) { + // TODO: export session if (Provider.TrySavePackage(entry, out var assets)) { string path = UserSettings.Default.RawDataDirectory; diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index 0d6ace36..bf35071f 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -2,14 +2,12 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.Nanite; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.Core.Serialization; using CUE4Parse.UE4.Versions; -using CUE4Parse_Conversion.Meshes; -using CUE4Parse_Conversion.Textures; -using CUE4Parse_Conversion.UEFormat.Enums; +using CUE4Parse_Conversion.Options; +using CUE4Parse_Conversion.Writers.UEFormat.Enums; +using CUE4Parse.UE4.Assets.Exports.Material; using FModel.Framework; using FModel.Services; using FModel.Settings; @@ -137,8 +135,8 @@ public class SettingsViewModel : ViewModel set => SetProperty(ref _selectedCompressionFormat, value); } - private ELodFormat _selectedLodExportFormat; - public ELodFormat SelectedLodExportFormat + private EMeshQuality _selectedLodExportFormat; + public EMeshQuality SelectedLodExportFormat { get => _selectedLodExportFormat; set => SetProperty(ref _selectedLodExportFormat, value); @@ -151,8 +149,8 @@ public class SettingsViewModel : ViewModel set => SetProperty(ref _selectedNaniteMeshExportFormat, value); } - private EMaterialFormat _selectedMaterialExportFormat; - public EMaterialFormat SelectedMaterialExportFormat + private EMaterialDepth _selectedMaterialExportFormat; + public EMaterialDepth SelectedMaterialExportFormat { get => _selectedMaterialExportFormat; set => SetProperty(ref _selectedMaterialExportFormat, value); @@ -191,9 +189,9 @@ public class SettingsViewModel : ViewModel public ReadOnlyObservableCollection MeshExportFormats { get; private set; } public ReadOnlyObservableCollection SocketExportFormats { get; private set; } public ReadOnlyObservableCollection CompressionFormats { get; private set; } - public ReadOnlyObservableCollection LodExportFormats { get; private set; } + public ReadOnlyObservableCollection LodExportFormats { get; private set; } public ReadOnlyObservableCollection NaniteMeshExportFormats { get; private set; } - public ReadOnlyObservableCollection MaterialExportFormats { get; private set; } + public ReadOnlyObservableCollection MaterialExportFormats { get; private set; } public ReadOnlyObservableCollection TextureExportFormats { get; private set; } public ReadOnlyObservableCollection Platforms { get; private set; } @@ -216,9 +214,9 @@ public class SettingsViewModel : ViewModel private EMeshFormat _meshExportFormatSnapshot; private ESocketFormat _socketExportFormatSnapshot; private EFileCompressionFormat _compressionFormatSnapshot; - private ELodFormat _lodExportFormatSnapshot; + private EMeshQuality _lodExportFormatSnapshot; private ENaniteMeshFormat _naniteMeshExportFormatSnapshot; - private EMaterialFormat _materialExportFormatSnapshot; + private EMaterialDepth _materialExportFormatSnapshot; private ETextureFormat _textureExportFormatSnapshot; private bool _mappingsUpdate = false; @@ -260,7 +258,7 @@ public class SettingsViewModel : ViewModel _meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat; _socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat; _compressionFormatSnapshot = UserSettings.Default.CompressionFormat; - _lodExportFormatSnapshot = UserSettings.Default.LodExportFormat; + _lodExportFormatSnapshot = UserSettings.Default.MeshQuality; _naniteMeshExportFormatSnapshot = UserSettings.Default.NaniteMeshExportFormat; _materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat; _textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat; @@ -294,9 +292,9 @@ public class SettingsViewModel : ViewModel MeshExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMeshExportFormat())); SocketExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateSocketExportFormat())); CompressionFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateCompressionFormat())); - LodExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateLodExportFormat())); + LodExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateLodExportFormat())); NaniteMeshExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateNaniteMeshExportFormat())); - MaterialExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMaterialExportFormat())); + MaterialExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateMaterialExportFormat())); TextureExportFormats = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateTextureExportFormat())); Platforms = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUePlatforms())); } @@ -331,7 +329,7 @@ public class SettingsViewModel : ViewModel UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat; UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat; UserSettings.Default.CompressionFormat = SelectedCompressionFormat; - UserSettings.Default.LodExportFormat = SelectedLodExportFormat; + UserSettings.Default.MeshQuality = SelectedLodExportFormat; UserSettings.Default.NaniteMeshExportFormat = SelectedNaniteMeshExportFormat; UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat; UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat; @@ -357,9 +355,9 @@ public class SettingsViewModel : ViewModel private IEnumerable EnumerateMeshExportFormat() => Enum.GetValues(); private IEnumerable EnumerateSocketExportFormat() => Enum.GetValues(); private IEnumerable EnumerateCompressionFormat() => Enum.GetValues(); - private IEnumerable EnumerateLodExportFormat() => Enum.GetValues(); + private IEnumerable EnumerateLodExportFormat() => Enum.GetValues(); private IEnumerable EnumerateNaniteMeshExportFormat() => Enum.GetValues(); - private IEnumerable EnumerateMaterialExportFormat() => Enum.GetValues(); + private IEnumerable EnumerateMaterialExportFormat() => Enum.GetValues(); private IEnumerable EnumerateTextureExportFormat() => Enum.GetValues(); private IEnumerable EnumerateUePlatforms() => Enum.GetValues(); } diff --git a/FModel/ViewModels/TabControlViewModel.cs b/FModel/ViewModels/TabControlViewModel.cs index dbeb4423..41df329f 100644 --- a/FModel/ViewModels/TabControlViewModel.cs +++ b/FModel/ViewModels/TabControlViewModel.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Windows; using System.Windows.Media.Imaging; +using CUE4Parse_Conversion.Options; using CUE4Parse.FileProvider.Objects; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.Utils; diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index c7e8a1bf..f3fe20f3 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:FModel" - xmlns:c4pMeshes="clr-namespace:CUE4Parse_Conversion.Meshes;assembly=CUE4Parse-Conversion" + xmlns:c4pMeshes="clr-namespace:CUE4Parse_Conversion.Options;assembly=CUE4Parse-Conversion" xmlns:controls="clr-namespace:FModel.Views.Resources.Controls" xmlns:converters="clr-namespace:FModel.Views.Resources.Converters" xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI" @@ -452,7 +452,7 @@ - + diff --git a/FModel/Views/Snooper/Animations/Animation.cs b/FModel/Views/Snooper/Animations/Animation.cs index d41afaa7..561a9121 100644 --- a/FModel/Views/Snooper/Animations/Animation.cs +++ b/FModel/Views/Snooper/Animations/Animation.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Numerics; using CUE4Parse_Conversion; -using CUE4Parse_Conversion.Animations.PSA; +using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Objects.Core.Misc; using FModel.Settings; @@ -12,7 +11,7 @@ using ImGuiNET; namespace FModel.Views.Snooper.Animations; -public class Animation : IDisposable +public class Animation : IExportableThing, IDisposable { private readonly UObject _export; @@ -82,7 +81,7 @@ public class Animation : IDisposable AttachedModels.Clear(); } - public void ImGuiAnimation(Snooper s, Save saver, ImDrawListPtr drawList, ImFontPtr fontPtr, Vector2 timelineP0, Vector2 treeP0, Vector2 timeStep, Vector2 timeRatio, float y, float t, int i) + public void ImGuiAnimation(Snooper s, ImDrawListPtr drawList, ImFontPtr fontPtr, Vector2 timelineP0, Vector2 treeP0, Vector2 timeStep, Vector2 timeRatio, float y, float t, int i) { var name = $"{Name}##{i}"; var p1 = new Vector2(timelineP0.X + StartTime * timeRatio.X + t, y + t); @@ -96,7 +95,7 @@ public class Animation : IDisposable { s.Renderer.Options.SelectAnimation(i); } - Popup(s, saver, i); + Popup(s, i); drawList.AddRectFilled(p1, p2, IsSelected ? 0xFF48B048 : 0xFF175F17, 5.0f, ImDrawFlags.RoundCornersTop); for (int j = 0; j < Sequences.Length; j++) @@ -109,10 +108,10 @@ public class Animation : IDisposable { s.Renderer.Options.SelectAnimation(i); } - Popup(s, saver, i); + Popup(s, i); } - private void Popup(Snooper s, Save saver, int i) + private void Popup(Snooper s, int i) { SnimGui.Popup(() => { @@ -134,11 +133,16 @@ public class Animation : IDisposable if (ImGui.MenuItem("Save")) { s.WindowShouldFreeze(true); - saver.Value = new Exporter(_export, UserSettings.Default.ExportOptions).TryWriteToDir(new DirectoryInfo(UserSettings.Default.ModelDirectory), out saver.Label, out saver.Path); + ExportModal.Instance.Export([this], UserSettings.Default.ModelDirectory, UserSettings.GetExportOptions()); s.WindowShouldFreeze(false); } ImGui.Separator(); if (ImGui.MenuItem("Copy Path to Clipboard")) ImGui.SetClipboardText(Path); }); } + + public void AddToExportSession(ExportSession session) + { + session.Add(_export); + } } diff --git a/FModel/Views/Snooper/Animations/Sequence.cs b/FModel/Views/Snooper/Animations/Sequence.cs index bb12b595..04bbf09c 100644 --- a/FModel/Views/Snooper/Animations/Sequence.cs +++ b/FModel/Views/Snooper/Animations/Sequence.cs @@ -1,5 +1,5 @@ using System.Numerics; -using CUE4Parse_Conversion.Animations.PSA; +using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations; using ImGuiNET; namespace FModel.Views.Snooper.Animations; diff --git a/FModel/Views/Snooper/Animations/Skeleton.cs b/FModel/Views/Snooper/Animations/Skeleton.cs index 2abc98c9..31a8ef4d 100644 --- a/FModel/Views/Snooper/Animations/Skeleton.cs +++ b/FModel/Views/Snooper/Animations/Skeleton.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -using CUE4Parse_Conversion.Animations.PSA; +using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations; using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Objects.Core.Misc; using FModel.Views.Snooper.Buffers; diff --git a/FModel/Views/Snooper/Animations/TimeTracker.cs b/FModel/Views/Snooper/Animations/TimeTracker.cs index 5775c08f..f482599d 100644 --- a/FModel/Views/Snooper/Animations/TimeTracker.cs +++ b/FModel/Views/Snooper/Animations/TimeTracker.cs @@ -61,7 +61,7 @@ public class TimeTracker : IDisposable } private readonly string[] _icons = { "tl_forward", "tl_pause", "tl_rewind" }; - public void ImGuiTimeline(Snooper s, Save saver, Dictionary icons, List animations, Vector2 outliner, ImFontPtr fontPtr) + public void ImGuiTimeline(Snooper s, Dictionary icons, List animations, Vector2 outliner, ImFontPtr fontPtr) { var dpiScale = ImGui.GetWindowDpiScale(); var thickness = 2.0f * dpiScale; @@ -151,7 +151,7 @@ public class TimeTracker : IDisposable for (int i = 0; i < animations.Count; i++) { var y = timelineP0.Y + timeBarHeight + timeStep.Y * i; - animations[i].ImGuiAnimation(s, saver, drawList, fontPtr, timelineP0, treeP0, timeStep, timeRatio, y, thickness, i); + animations[i].ImGuiAnimation(s, drawList, fontPtr, timelineP0, treeP0, timeStep, timeRatio, y, thickness, i); DrawSeparator(drawList, timelineP0, y + timeStep.Y, animations[i].EndTime * timeRatio.X, timeHeight, timeBarHeight, ETrackerType.End); } ImGui.PopStyleVar(); diff --git a/FModel/Views/Snooper/Buffers/PickingTexture.cs b/FModel/Views/Snooper/Buffers/PickingTexture.cs index c7b1fbe4..9ee999cb 100644 --- a/FModel/Views/Snooper/Buffers/PickingTexture.cs +++ b/FModel/Views/Snooper/Buffers/PickingTexture.cs @@ -54,7 +54,7 @@ public class PickingTexture : IDisposable Bind(0); } - public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix, IDictionary models) + public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix, IDictionary models) { Bind(); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); diff --git a/FModel/Views/Snooper/ExportModal.cs b/FModel/Views/Snooper/ExportModal.cs new file mode 100644 index 00000000..53132514 --- /dev/null +++ b/FModel/Views/Snooper/ExportModal.cs @@ -0,0 +1,662 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; +using CUE4Parse_Conversion; +using CUE4Parse_Conversion.Options; +using FModel.Extensions; +using FModel.Views.Snooper.Models; +using ImGuiNET; +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace FModel.Views.Snooper; + +public sealed class ExportModal +{ + public static ExportModal Instance { get; } = new(); + + private const string Title = "Export Progress"; + private const string IconXMark = "\uf057"; + private const string IconFolder = "\uf08e"; + + private readonly Vector4[] _pieColors = + [ + new(0.22f, 0.52f, 0.90f, 1f), + new(0.28f, 0.78f, 0.44f, 1f), + new(0.90f, 0.62f, 0.22f, 1f), + new(0.75f, 0.32f, 0.75f, 1f), + new(0.32f, 0.75f, 0.85f, 1f), + new(0.85f, 0.32f, 0.45f, 1f), + new(0.90f, 0.90f, 0.22f, 1f), + new(0.55f, 0.75f, 0.32f, 1f), + ]; + + private static readonly Vector4 _redColor = new(1f, 0.4f, 0.4f, 1f); + private static readonly Vector4 _orangeColor = new(1f, 0.5f, 0f, 1f); + private static readonly Vector4 _yellowColor = new(1f, 1f, 0.4f, 1f); + private static readonly Vector4 _greenColor = new(0.4f, 1f, 0.4f, 1f); + + private bool _openPopup; + private bool _modalOpen; + private bool _inProgress; + private CancellationTokenSource? _cts; + private IReadOnlyList? _exportResults; + + private ExportProgress _currentProgress; + private readonly IProgress _progress; + private readonly Stopwatch _stopwatch = new(); + private readonly ConcurrentQueue _pendingLogs = new(); + private readonly List _classGroups = []; + + private const int MaxGraphSamples = 4096; + private const float GraphSampleIntervalSec = 0.25f; + private readonly List _graphSamples = []; + private float _graphNextSampleAt; + private int _graphLastCompleted; + + private ExportModal() + { + ImGuiSink.Instance.OnExporterLogEvent += _pendingLogs.Enqueue; + _progress = new Progress(p => _currentProgress = p); + } + + public void Export(IEnumerable nodes, string exportDirectory, ExportOptions options) + { + Reset(); + _openPopup = true; + _inProgress = true; + _cts = new CancellationTokenSource(); + _stopwatch.Restart(); + + var token = _cts.Token; + _ = Task.Run(async () => + { + try + { + var session = new ExportSession(exportDirectory, options); + foreach (var node in nodes) node.AddToExportSession(session); + _exportResults = await session.RunAsync(_progress, token); + } + catch (OperationCanceledException) + { + Log.Error("Export cancelled by user"); + } + catch (Exception ex) + { + Log.Error(ex, "Export failed"); + } + finally + { + _stopwatch.Stop(); + _inProgress = false; + } + }, token); + } + + public void Draw() + { + if (_openPopup) + { + ImGui.OpenPopup(Title); + _modalOpen = true; + _openPopup = false; + } + + if (!_modalOpen) return; + + var viewport = ImGui.GetMainViewport(); + ImGui.SetNextWindowSize(viewport.WorkSize * 0.75f, ImGuiCond.Always); + ImGui.SetNextWindowPos(viewport.GetCenter(), ImGuiCond.Always, new Vector2(0.5f, 0.5f)); + + var open = true; + if (ImGui.BeginPopupModal(Title, ref open, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize)) + { + if (ImGui.BeginChild("##ModalInfoBody", Vector2.Zero, ImGuiChildFlags.FrameStyle)) + { + DrawProgressBar(); + + ImGui.Spacing(); + ImGui.SeparatorText("Throughput"); + DrawThroughputGraph(); + + ImGui.Spacing(); + ImGui.SeparatorText("Export Log"); + DrawExportLog(); + } + ImGui.EndChild(); + ImGui.EndPopup(); + } + + if (!open) + { + _modalOpen = false; + Reset(); + } + } + + private void Reset() + { + _pendingLogs.Clear(); + _classGroups.Clear(); + _exportResults = null; + _currentProgress = new ExportProgress(0, 0); + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + + _graphSamples.Clear(); + _graphNextSampleAt = 0f; + _graphLastCompleted = 0; + } + + private void DrawProgressBar() + { + var e = _stopwatch.Elapsed; + ImGui.TextDisabled("\uf2f2"); + ImGui.SameLine(); + ImGui.TextUnformatted($"{e.Minutes:D2}:{e.Seconds:D2}.{e.Milliseconds / 10:D2}"); + + if (_inProgress && _currentProgress is { Total: > 0, Completed: > 1 }) + { + var rate = _currentProgress.Completed / e.TotalSeconds; + if (rate > 0) + { + var remaining = (_currentProgress.Total - _currentProgress.Completed) / rate; + var eta = TimeSpan.FromSeconds(remaining); + ImGui.SameLine(); + ImGui.TextDisabled("\uf017"); + ImGui.SameLine(); + ImGui.TextUnformatted($"ETA {eta.Minutes:D2}:{eta.Seconds:D2}"); + } + } + else if (!_inProgress && _exportResults is { Count: > 0 }) + { + ImGui.SameLine(); + ImGui.TextColored(_greenColor, "\uf058"); + ImGui.SameLine(); + ImGui.TextUnformatted($"{_exportResults?.Count(r => r.Success) ?? 0} succeeded"); + + ImGui.SameLine(); + ImGui.TextColored(_redColor, IconXMark); + ImGui.SameLine(); + ImGui.TextUnformatted($"{_exportResults?.Count(r => !r.Success) ?? 0} failed"); + } + + ImGui.Spacing(); + + var barColor = _classGroups.Any(cg => cg.ErrorCount > 0) ? new Vector4(0.75f, 0.32f, 0.32f, 1f) : _inProgress ? new Vector4(0.22f, 0.52f, 0.90f, 1f) : new Vector4(0.28f, 0.78f, 0.44f, 1f); + var label = _inProgress && _currentProgress.Total > 0 ? _currentProgress.DisplayText : _inProgress ? "Preparing..." : "Done"; + + var barPos = ImGui.GetCursorScreenPos(); + var barSize = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetFrameHeight()); + var hovered = ImGui.IsMouseHoveringRect(barPos, barPos + barSize); + if (hovered && _inProgress) + { + barColor = new Vector4(0.75f, 0.32f, 0.32f, 1f); + label = "\uf05e Cancel"; + } + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 4f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f); + ImGui.PushStyleColor(ImGuiCol.PlotHistogram, barColor); + ImGui.ProgressBar(_currentProgress.Percentage, barSize, label); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(2); + + if (_inProgress) + { + ImGui.SetCursorScreenPos(barPos); + if (ImGui.InvisibleButton("##BarAction", barSize)) + { + _cts?.Cancel(); + } + } + } + + private void DrawThroughputGraph() + { + var elapsedSec = (float)_stopwatch.Elapsed.TotalSeconds; + if (_inProgress && _currentProgress.Completed > 0 && elapsedSec >= _graphNextSampleAt && _graphSamples.Count < MaxGraphSamples) + { + var completed = _currentProgress.Completed; + var rate = (completed - _graphLastCompleted) / GraphSampleIntervalSec; + _graphSamples.Add(MathF.Max(rate, 0f)); + _graphLastCompleted = completed; + _graphNextSampleAt = elapsedSec + GraphSampleIntervalSec; + } + + var graphPos = ImGui.GetCursorScreenPos(); + var graphW = ImGui.GetContentRegionAvail().X; + var graphH = ImGui.GetFrameHeight() * 3f; + var graphSize = new Vector2(graphW, graphH); + ImGui.InvisibleButton("##ThroughputGraph", graphSize); + var dl = ImGui.GetWindowDrawList(); + + dl.AddRectFilled(graphPos, graphPos + graphSize, 0xFF_14_14_14); + + var count = _graphSamples.Count; + switch (count) + { + case 0 when !_inProgress: + { + dl.AddRect(graphPos, graphPos + graphSize, 0xFF_2A_2A_2A); + return; + } + case < 2: + { + if (_inProgress) + { + const string msg = "Collecting data..."; + var msgSz = ImGui.CalcTextSize(msg); + dl.AddText(graphPos + (graphSize - msgSz) * 0.5f, 0x44_FF_FF_FF, msg); + } + dl.AddRect(graphPos, graphPos + graphSize, 0xFF_2A_2A_2A); + return; + } + } + + // Y scale + var maxVal = 0f; + for (var i = 0; i < count; i++) maxVal = MathF.Max(maxVal, _graphSamples[i]); + if (maxVal < 0.01f) maxVal = 1f; + maxVal *= 1.15f; // top headroom + + dl.PushClipRect(graphPos, graphPos + graphSize, true); + + // Horizontal grid lines at 25 / 50 / 75 % + for (var g = 1; g < 4; g++) + { + var gy = graphPos.Y + graphH * g / 4f; + dl.AddLine(new Vector2(graphPos.X, gy), new Vector2(graphPos.X + graphW, gy), 0x18_FF_FF_FF, 0.5f); + } + + var lineColor = ImGui.GetColorU32(new Vector4(0.22f, 0.52f, 0.90f, 1f)); + var fillColor = ImGui.GetColorU32(new Vector4(0.22f, 0.52f, 0.90f, 0.15f)); + + // xStep shrinks as samples accumulate → graph zooms out naturally. + // Oldest sample is always at x=0, newest at x=graphW. + var xStep = graphW / (count - 1f); + + // Filled area (series of convex quads) + for (var i = 0; i < count - 1; i++) + { + var t0 = Math.Clamp(_graphSamples[i] / maxVal, 0f, 1f); + var t1 = Math.Clamp(_graphSamples[i + 1] / maxVal, 0f, 1f); + var x0 = graphPos.X + xStep * i; + var x1 = graphPos.X + xStep * (i + 1); + var y0 = graphPos.Y + graphH - graphH * t0; + var y1 = graphPos.Y + graphH - graphH * t1; + var bot = graphPos.Y + graphH; + dl.AddQuadFilled(new Vector2(x0, y0), new Vector2(x1, y1), new Vector2(x1, bot), new Vector2(x0, bot), fillColor); + } + + // Line + for (var i = 0; i < count - 1; i++) + { + var t0 = Math.Clamp(_graphSamples[i] / maxVal, 0f, 1f); + var t1 = Math.Clamp(_graphSamples[i + 1] / maxVal, 0f, 1f); + var x0 = graphPos.X + xStep * i; + var x1 = graphPos.X + xStep * (i + 1); + var y0 = graphPos.Y + graphH - graphH * t0; + var y1 = graphPos.Y + graphH - graphH * t1; + dl.AddLine(new Vector2(x0, y0), new Vector2(x1, y1), lineColor, 1.5f); + } + + // Dot on the newest sample (always at the right edge) + var newestT = Math.Clamp(_graphSamples[count - 1] / maxVal, 0f, 1f); + var newestY = graphPos.Y + graphH - graphH * newestT; + dl.AddCircleFilled(new Vector2(graphPos.X + graphW, newestY), 3f, lineColor); + + var rateStr = $"{_graphSamples[count - 1]:F1} items/s"; + dl.AddText(new Vector2(graphPos.X + 4, graphPos.Y + 3), 0xCC_FF_FF_FF, rateStr); + + dl.PopClipRect(); + + dl.AddRect(graphPos, graphPos + graphSize, 0xFF_2A_2A_2A); + } + + private void DrawExportLog() + { + var avail = ImGui.GetContentRegionAvail(); + var rowH = ImGui.GetTextLineHeightWithSpacing(); + var canvasSize = 15 * rowH + ImGui.GetFrameHeightWithSpacing(); + var treeW = avail.X - canvasSize - ImGui.GetStyle().ItemSpacing.X; + + DrainPendingLogs(); + + if (ImGui.BeginChild("##ExportLogTree", avail with { X = treeW }, ImGuiChildFlags.FrameStyle)) + { + if (_classGroups.Count == 0) + { + ImGui.TextDisabled(_inProgress ? "Waiting for export data..." : "No export log."); + } + else for (var i = 0; i < _classGroups.Count; i++) + { + DrawClassGroup(i, _classGroups[i]); + } + } + ImGui.EndChild(); + + ImGui.SameLine(); + if (ImGui.BeginChild("##RightPanel", new Vector2(canvasSize, -1), ImGuiChildFlags.FrameStyle)) + { + DrawPieCanvas(); + + if (_classGroups.Count == 0) + { + ImGui.TextDisabled(_inProgress ? "Waiting for data..." : "No export data."); + } + else + { + var total = _classGroups.Sum(cg => cg.Objects.Count); + for (var i = 0; i < _classGroups.Count; i++) + { + var cg = _classGroups[i]; + ImGui.PushStyleColor(ImGuiCol.Text, _pieColors[i % _pieColors.Length]); + ImGui.TextUnformatted("\uf111"); + ImGui.PopStyleColor(); + ImGui.SameLine(); + ImGui.TextUnformatted(cg.Name); + ImGui.SameLine(); + ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImGui.TextUnformatted($"({(float) cg.Objects.Count / total * 100f:F1}%)"); + ImGui.PopStyleColor(); + } + } + } + + ImGui.EndChild(); + } + + private void DrawPieCanvas() + { + const int segments = 64; + + var canvasPos = ImGui.GetCursorScreenPos(); + var size = ImGui.GetContentRegionAvail().X; + var canvasVec = new Vector2(size); + ImGui.InvisibleButton("##PieCanvas", canvasVec); + var isHovered = ImGui.IsItemHovered(); + var mousePos = ImGui.GetMousePos(); + var dl = ImGui.GetWindowDrawList(); + + dl.AddRectFilled(canvasPos, canvasPos + canvasVec, 0xFF_14_14_14); + dl.AddRect(canvasPos, canvasPos + canvasVec, 0xFF_32_32_32); + + var total = _classGroups.Sum(cg => cg.Objects.Count); + var padding = ImGui.GetFrameHeight() * 0.5f; + var radius = size * 0.5f - padding; + var center = canvasPos + new Vector2(size * 0.5f); + + if (total == 0 || radius <= 0) + { + dl.AddCircleFilled(center, MathF.Max(radius, 1f), 0xFF_1F_1F_1F, segments); + return; + } + + // Determine hovered slice by angle + var hoveredSlice = -1; + if (isHovered) + { + var dx = mousePos.X - center.X; + var dy = mousePos.Y - center.Y; + if (dx * dx + dy * dy <= radius * radius) + { + var angle = MathF.Atan2(dy, dx); + while (angle < -MathF.PI / 2f) angle += MathF.PI * 2f; + var cur = -MathF.PI / 2f; + for (var i = 0; i < _classGroups.Count; i++) + { + var sweep = (float)_classGroups[i].Objects.Count / total * MathF.PI * 2f; + if (angle >= cur && angle < cur + sweep) { hoveredSlice = i; break; } + cur += sweep; + } + } + } + + float startAngle = -MathF.PI / 2f; + for (var i = 0; i < _classGroups.Count; i++) + { + var cg = _classGroups[i]; + var ratio = (float) cg.Objects.Count / total; + var sliceAngle = ratio * MathF.PI * 2f; + var col = ImGui.GetColorU32(_pieColors[i % _pieColors.Length]); + var r = i == hoveredSlice ? radius + padding * 0.25f : radius; + dl.PathLineTo(center); + dl.PathArcTo(center, r, startAngle, startAngle + sliceAngle); + dl.PathFillConvex(col); + + var midAngle = startAngle + sliceAngle * 0.5f; + var labelPos = center + new Vector2(MathF.Cos(midAngle), MathF.Sin(midAngle)) * (radius * 0.62f); + var pctStr = $"{ratio * 100f:F0}%"; + dl.AddText(labelPos - ImGui.CalcTextSize(pctStr) * 0.5f, 0xFF_FF_FF_FF, pctStr); + + startAngle += sliceAngle; + } + + dl.AddCircle(center, radius, 0xAA_00_00_00, segments, 1.5f); + + if (hoveredSlice >= 0) + { + ImGui.BeginTooltip(); + + var cg = _classGroups[hoveredSlice]; + ImGui.PushStyleColor(ImGuiCol.Text, _pieColors[hoveredSlice % _pieColors.Length]); + ImGui.TextUnformatted("\uf111"); + ImGui.PopStyleColor(); + ImGui.SameLine(); + ImGui.TextUnformatted(cg.Name); + + ImGui.EndTooltip(); + } + } + + private void DrawClassGroup(int index, ClassGroup cg) + { + ImGui.PushStyleColor(ImGuiCol.Header, new Vector4(0.20f, 0.20f, 0.20f, 1.00f)); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, new Vector4(0.69f, 0.69f, 1.00f, 0.20f)); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, new Vector4(0.69f, 0.69f, 1.00f, 0.20f)); + var open = ImGui.CollapsingHeader($"{cg.Name} ({cg.Objects.Count})##class_{cg.Name}"); + ImGui.PopStyleColor(3); + var headerMin = ImGui.GetItemRectMin(); + var headerMax = ImGui.GetItemRectMax(); + + var labelW = MathF.Floor(ImGui.GetStyle().ItemSpacing.X * 0.5f); + var col = ImGui.GetColorU32(_pieColors[index % _pieColors.Length]); + ImGui.GetWindowDrawList().AddRectFilled(headerMin, headerMax with { X = headerMin.X + labelW }, col); + + if (cg.ErrorCount > 0 && ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"{cg.ErrorCount} error{(cg.ErrorCount > 1 ? "s" : "")} in this class"); + } + + if (!open) return; + foreach (var og in cg.Objects) + { + DrawObjectGroup(cg.Name, og); + } + } + + private void DrawObjectGroup(string className, ObjectGroup og) + { + var rightEdge = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X; + + var hasErr = og.ErrorCount > 0; + if (hasErr) ImGui.PushStyleColor(ImGuiCol.Text, _redColor); + var flags = ImGuiTreeNodeFlags.AllowOverlap | ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.FramePadding; + var open = ImGui.TreeNodeEx($"{og.Name}##obj_{className}_{og.Name}", flags); + if (hasErr) ImGui.PopStyleColor(); + + if (og.Entries.FirstOrDefault(e => !string.IsNullOrEmpty(e.FilePath)) is { } first) + { + var style = ImGui.GetStyle(); + var btnW = ImGui.CalcTextSize(IconFolder).X + style.FramePadding.X * 2; + ImGui.SameLine(rightEdge - btnW); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, style.ItemSpacing with { X = 0 }); + ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); + if (ImGui.Button($"{IconFolder}##obj_{className}_{og.Name}")) + { + OpenInExplorer(first.FilePath!); + } + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Open In Explorer"); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + if (!open) return; + foreach (var entry in og.Entries) + { + DrawLogEntry(entry); + } + ImGui.TreePop(); + } + + private void DrawLogEntry(LogEntry entry) + { + ImGui.PushStyleColor(ImGuiCol.Text, entry.Color); + ImGui.TextUnformatted(entry.Icon); + ImGui.PopStyleColor(); + ImGui.SameLine(); + ImGui.TextUnformatted(entry.Message); + if (ImGui.IsItemHovered() && entry.Exception != null) + { + DrawExceptionTooltip(entry.Exception); + } + } + + private static void DrawExceptionTooltip(Exception ex) + { + ImGui.BeginTooltip(); + ImGui.PushStyleColor(ImGuiCol.Text, _redColor); + ImGui.TextUnformatted(ex.GetType().ToString()); + ImGui.PopStyleColor(); + ImGui.SameLine(0, 0); + ImGui.TextDisabled(":"); + ImGui.SameLine(); + ImGui.TextUnformatted(ex.Message); + if (!string.IsNullOrEmpty(ex.StackTrace)) + { + ImGui.SetWindowFontScale(0.85f); + ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImGui.TextUnformatted(ex.StackTrace); + ImGui.PopStyleColor(); + ImGui.SetWindowFontScale(1.0f); + } + ImGui.EndTooltip(); + } + + private void OpenInExplorer(string path) + { + try + { + if (File.Exists(path) || Directory.Exists(path)) Process.Start("explorer.exe", $"/select, \"{path}\""); + else Log.Warning("File or directory does not exist: {Path}", path); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to open in explorer: {Path}", path); + } + } + + private void DrainPendingLogs() + { + while (_pendingLogs.TryDequeue(out var log)) + { + var className = log.GetContext("ClassName"); + var objectName = log.GetContext("ObjectName"); + var filePath = log.GetContext("FilePath"); + + var cg = FindOrCreateClass(className); + var og = FindOrCreateObject(cg, objectName); + var entry = new LogEntry(log, filePath); + if (entry.Icon == IconXMark) + { + og.ErrorCount++; + cg.ErrorCount++; + } + og.Entries.Add(entry); + } + } + + private ClassGroup FindOrCreateClass(string name) + { + foreach (var cg in _classGroups) + if (cg.Name == name) return cg; + + var n = new ClassGroup(name); + _classGroups.Add(n); + return n; + } + + private static ObjectGroup FindOrCreateObject(ClassGroup cg, string name) + { + foreach (var og in cg.Objects) + if (og.Name == name) return og; + + var n = new ObjectGroup(name); + cg.Objects.Add(n); + return n; + } + + private sealed class LogEntry(LogEvent log, string? filePath) + { + public string Icon { get; } = log.Level switch + { + LogEventLevel.Error or LogEventLevel.Fatal => IconXMark, + LogEventLevel.Warning => "\uf071", + LogEventLevel.Information => "\uf05a", + LogEventLevel.Debug => "\uf188", + _ => "\uf5dc" + }; + public Vector4 Color { get; } = log.Level switch + { + LogEventLevel.Error or LogEventLevel.Fatal => _redColor, + LogEventLevel.Warning => _yellowColor, + _ => new Vector4(0.5f, 0.5f, 0.5f, 1f) + }; + public string Message { get; } = $"[{log.Timestamp:HH:mm:ss.fff}] {log.RenderMessage()}"; + public string? FilePath { get; } = filePath; + public Exception? Exception { get; } = log.Exception; + } + + private sealed class ObjectGroup(string name) + { + public string Name { get; } = name; + public List Entries { get; } = []; + public int ErrorCount { get; set; } + } + + private sealed class ClassGroup(string name) + { + public string Name { get; } = name; + public List Objects { get; } = []; + public int ErrorCount { get; set; } + } +} + +public class ImGuiSink : ILogEventSink +{ + public static ImGuiSink Instance { get; } = new(); + + private ImGuiSink() + { + + } + + public event Action? OnExporterLogEvent; + + public void Emit(LogEvent logEvent) + { + if (logEvent.Properties.TryGetValue("ExporterV2", out var state) && state is ScalarValue { Value: true }) + { + OnExporterLogEvent?.Invoke(logEvent); + } + } +} diff --git a/FModel/Views/Snooper/Models/Attachment.cs b/FModel/Views/Snooper/Models/Attachment.cs index f1f53aa0..1031a35b 100644 --- a/FModel/Views/Snooper/Models/Attachment.cs +++ b/FModel/Views/Snooper/Models/Attachment.cs @@ -24,7 +24,7 @@ public class Attachment _attachedFor = new List(); } - public void Attach(UModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info) + public void Attach(IRenderableModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info) { socket.AttachedModels.Add(info); @@ -38,13 +38,13 @@ public class Attachment transform.Scale = FVector.OneVector; } - public void Detach(UModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info) + public void Detach(IRenderableModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info) { socket.AttachedModels.Remove(info); SafeDetach(attachedTo, transform); } - public void SafeDetach(UModel attachedTo, Transform transform) + public void SafeDetach(IRenderableModel attachedTo, Transform transform) { _attachedTo = string.Empty; attachedTo.Attachments.RemoveAttachment(_modelName); diff --git a/FModel/Views/Snooper/Models/IRenderableModel.cs b/FModel/Views/Snooper/Models/IRenderableModel.cs index 17fde580..aff17989 100644 --- a/FModel/Views/Snooper/Models/IRenderableModel.cs +++ b/FModel/Views/Snooper/Models/IRenderableModel.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Numerics; +using CUE4Parse_Conversion; +using CUE4Parse.UE4.Objects.Core.Math; using FModel.Views.Snooper.Buffers; using FModel.Views.Snooper.Shading; namespace FModel.Views.Snooper.Models; -public interface IRenderableModel : IDisposable +public interface IRenderableModel : IExportableThing, IDisposable { protected int Handle { get; set; } protected BufferObject Ebo { get; set; } @@ -24,15 +26,36 @@ public interface IRenderableModel : IDisposable public List Transforms { get; } public Attachment Attachments { get; } + public FBox Box { get; protected init; } + public List Sockets { get; } + public List Collisions { get; } + public Material[] Materials { get; protected init; } + public bool IsTwoSided { get; internal set; } + public bool IsProp { get; internal set; } + + public bool HasSockets { get; } + public bool HasCollisions { get; } + public int TransformsCount { get; } + public bool IsSetup { get; set; } public bool IsVisible { get; set; } public bool IsSelected { get; set; } public bool ShowWireframe { get; set; } + public bool ShowCollisions { get; set; } + public int SelectedInstance { get; set; } public void Setup(Options options); public void SetupInstances(); public void Render(Shader shader, Texture checker = null, bool outline = false); + public void RenderCollision(Shader shader); public void PickingRender(Shader shader); public void Update(Options options); public void AddInstance(Transform transform); + + public Transform GetTransform(); +} + +public interface IExportableThing +{ + public void AddToExportSession(ExportSession session); } diff --git a/FModel/Views/Snooper/Models/Morph.cs b/FModel/Views/Snooper/Models/Morph.cs index a53aeb09..c769b359 100644 --- a/FModel/Views/Snooper/Models/Morph.cs +++ b/FModel/Views/Snooper/Models/Morph.cs @@ -61,7 +61,7 @@ public class Morph : IDisposable } } - public Morph(float[] vertices, Dictionary dict, UMorphTarget morphTarget, int index = 0) + public Morph(float[] vertices, Dictionary dict, UMorphTarget morphTarget, uint index = 0) { Name = morphTarget.Name; Vertices = new float[vertices.Length]; diff --git a/FModel/Views/Snooper/Models/SkeletalModel.cs b/FModel/Views/Snooper/Models/SkeletalModel.cs index 6b5c2dc2..1e4c34fc 100644 --- a/FModel/Views/Snooper/Models/SkeletalModel.cs +++ b/FModel/Views/Snooper/Models/SkeletalModel.cs @@ -1,7 +1,6 @@ -using System; using System.Collections.Generic; using System.Numerics; -using CUE4Parse_Conversion.Meshes.PSK; +using CUE4Parse_Conversion.Dto; using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Objects.Core.Math; @@ -14,7 +13,7 @@ using OpenTK.Graphics.OpenGL4; namespace FModel.Views.Snooper.Models; -public class SkeletalModel : UModel +public class SkeletalModel : UModel { private BufferObject _morphVbo; @@ -25,10 +24,10 @@ public class SkeletalModel : UModel public float MorphTime; - public SkeletalModel(USkeletalMesh export, CSkeletalMesh skeletalMesh, Transform transform = null) - : base(export, skeletalMesh.LODs[LodLevel], export.Materials, skeletalMesh.LODs[LodLevel].Verts, skeletalMesh.LODs.Count, transform) + public SkeletalModel(USkeletalMesh export, SkeletalMeshDto skeletalMesh, Transform transform = null) + : base(export, skeletalMesh.LODs[LodLevel], export.Materials, skeletalMesh.LODs[LodLevel].Vertices, skeletalMesh.LODs.Count, transform) { - Box = skeletalMesh.BoundingBox * Constants.SCALE_DOWN_RATIO; + Box = skeletalMesh.Bounds * Constants.SCALE_DOWN_RATIO; Skeleton = new Skeleton(export.ReferenceSkeleton); var sockets = new List(); @@ -101,10 +100,10 @@ public class SkeletalModel : UModel foreach (var morph in export.MorphTargets) { if (!morph.TryLoad(out UMorphTarget morphTarget) || morphTarget.MorphLODModels.Length < 1 || - morphTarget.MorphLODModels[skeletalMesh.LODs[LodLevel].LODIndex].Vertices.Length < 1) + morphTarget.MorphLODModels[skeletalMesh.LODs[LodLevel].SourceLodIndex].Vertices.Length < 1) continue; - Morphs.Add(new Morph(cachedVertices, vertexLookup, morphTarget, skeletalMesh.LODs[LodLevel].LODIndex)); + Morphs.Add(new Morph(cachedVertices, vertexLookup, morphTarget, skeletalMesh.LODs[LodLevel].SourceLodIndex)); } } diff --git a/FModel/Views/Snooper/Models/SplineModel.cs b/FModel/Views/Snooper/Models/SplineModel.cs index 1012707e..2a11919e 100644 --- a/FModel/Views/Snooper/Models/SplineModel.cs +++ b/FModel/Views/Snooper/Models/SplineModel.cs @@ -1,11 +1,8 @@ using System.Collections.Generic; -using System.Numerics; using System.Runtime.InteropServices; -using CUE4Parse_Conversion.Meshes.PSK; +using CUE4Parse_Conversion.Dto; using CUE4Parse.UE4.Assets.Exports.Component.SplineMesh; -using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.StaticMesh; -using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Objects.Core.Math; using FModel.Views.Snooper.Buffers; using FModel.Views.Snooper.Shading; @@ -74,7 +71,7 @@ public class SplineModel : StaticModel private readonly List _splineParams; private BufferObject _ssbo; - public SplineModel(UStaticMesh export, CStaticMesh staticMesh, USplineMeshComponent splineMesh, Transform transform = null) : base(export, staticMesh, transform) + public SplineModel(UStaticMesh export, StaticMeshDto staticMesh, USplineMeshComponent splineMesh, Transform transform = null) : base(export, staticMesh, transform) { _splineParams = [new GpuParams(splineMesh)]; diff --git a/FModel/Views/Snooper/Models/StaticModel.cs b/FModel/Views/Snooper/Models/StaticModel.cs index 4abb4c63..dcb654b4 100644 --- a/FModel/Views/Snooper/Models/StaticModel.cs +++ b/FModel/Views/Snooper/Models/StaticModel.cs @@ -1,6 +1,6 @@ using System; using System.Numerics; -using CUE4Parse_Conversion.Meshes.PSK; +using CUE4Parse_Conversion.Dto; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.Texture; @@ -11,24 +11,24 @@ using OpenTK.Graphics.OpenGL4; namespace FModel.Views.Snooper.Models; -public class StaticModel : UModel +public class StaticModel : UModel { - public StaticModel(UMaterialInterface unrealMaterial, CStaticMesh staticMesh) : base(unrealMaterial) + public StaticModel(UMaterialInterface unrealMaterial, StaticMeshDto staticMesh) : base(unrealMaterial) { var lod = staticMesh.LODs[LodLevel]; - Indices = new uint[lod.Indices.Value.Length]; + Indices = new uint[lod.Indices.Length]; for (int i = 0; i < Indices.Length; i++) { - Indices[i] = (uint) lod.Indices.Value[i]; + Indices[i] = lod.Indices[i]; } - Vertices = new float[lod.NumVerts * VertexSize]; - for (int i = 0; i < lod.Verts.Length; i++) + Vertices = new float[lod.Vertices.Length * VertexSize]; + for (int i = 0; i < lod.Vertices.Length; i++) { var count = 0; var baseIndex = i * VertexSize; - var vert = lod.Verts[i]; + var vert = lod.Vertices[i]; Vertices[baseIndex + count++] = i; Vertices[baseIndex + count++] = vert.Position.X * Constants.SCALE_DOWN_RATIO; Vertices[baseIndex + count++] = vert.Position.Z * Constants.SCALE_DOWN_RATIO; @@ -39,8 +39,8 @@ public class StaticModel : UModel Vertices[baseIndex + count++] = vert.Tangent.X; Vertices[baseIndex + count++] = vert.Tangent.Z; Vertices[baseIndex + count++] = vert.Tangent.Y; - Vertices[baseIndex + count++] = vert.UV.U; - Vertices[baseIndex + count++] = vert.UV.V; + Vertices[baseIndex + count++] = vert.Uv.U; + Vertices[baseIndex + count++] = vert.Uv.V; Vertices[baseIndex + count++] = .5f; } @@ -52,7 +52,7 @@ public class StaticModel : UModel AddInstance(Transform.Identity); - Box = staticMesh.BoundingBox * 1.5f * Constants.SCALE_DOWN_RATIO; + Box = staticMesh.Bounds * 1.5f * Constants.SCALE_DOWN_RATIO; } public StaticModel(UPaperSprite paperSprite, UTexture2D texture) : base(paperSprite) @@ -108,8 +108,8 @@ public class StaticModel : UModel Box = new FBox(-backward, backward) * Constants.SCALE_DOWN_RATIO; } - public StaticModel(UStaticMesh export, CStaticMesh staticMesh, Transform transform = null) - : base(export, staticMesh.LODs[LodLevel], export.Materials, staticMesh.LODs[LodLevel].Verts, staticMesh.LODs.Count, transform) + public StaticModel(UStaticMesh export, StaticMeshDto staticMesh, Transform transform = null) + : base(export, staticMesh.LODs[LodLevel], export.Materials, staticMesh.LODs[LodLevel].Vertices, staticMesh.LODs.Count, transform) { if (export.BodySetup.TryLoad(out UBodySetup bodySetup) && bodySetup.AggGeom != null) { @@ -135,7 +135,7 @@ public class StaticModel : UModel } } - Box = staticMesh.BoundingBox * Constants.SCALE_DOWN_RATIO; + Box = staticMesh.Bounds * Constants.SCALE_DOWN_RATIO; for (int i = 0; i < export.Sockets.Length; i++) { if (export.Sockets[i].Load() is not { } socket) continue; diff --git a/FModel/Views/Snooper/Models/UModel.cs b/FModel/Views/Snooper/Models/UModel.cs index 613d4100..9fa2a11d 100644 --- a/FModel/Views/Snooper/Models/UModel.cs +++ b/FModel/Views/Snooper/Models/UModel.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; using CUE4Parse_Conversion; -using CUE4Parse_Conversion.Meshes.PSK; -using CUE4Parse.UE4.Assets; +using CUE4Parse_Conversion.Dto; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Objects.Core.Math; -using CUE4Parse.Utils; -using FModel.Settings; +using CUE4Parse.UE4.Objects.UObject; using FModel.Views.Snooper.Buffers; using FModel.Views.Snooper.Shading; using OpenTK.Graphics.OpenGL4; @@ -24,7 +21,7 @@ public class VertexAttribute public bool Enabled; } -public abstract class UModel : IRenderableModel +public abstract class UModel : IRenderableModel where TVertex : struct, IMeshVertex { protected const int LodLevel = 0; @@ -58,12 +55,12 @@ public abstract class UModel : IRenderableModel public List Transforms { get; } public Attachment Attachments { get; } - public FBox Box; - public readonly List Sockets; - public readonly List Collisions; - public Material[] Materials; - public bool IsTwoSided; - public bool IsProp; + public FBox Box { get; init; } + public List Sockets { get; } + public List Collisions { get; } + public Material[] Materials { get; init; } + public bool IsTwoSided { get; set; } + public bool IsProp { get; set; } public int VertexSize => _vertexAttributes.Where(x => x.Enabled).Sum(x => x.Size); public bool HasVertexColors => _vertexAttributes[(int) EAttribute.Colors].Enabled; @@ -76,7 +73,7 @@ public abstract class UModel : IRenderableModel public bool IsSelected { get; set; } public bool ShowWireframe { get; set; } public bool ShowCollisions { get; set; } - public int SelectedInstance; + public int SelectedInstance { get; set; } protected UModel() { @@ -110,16 +107,16 @@ public abstract class UModel : IRenderableModel _vertexAttributes[(int) EAttribute.Layer].Enabled = true; } - protected UModel(UObject export, CBaseMeshLod lod, IReadOnlyList materials, IReadOnlyList vertices, int numLods, Transform transform = null) : this(export) + protected UModel(UObject export, MeshLodDto lod, IReadOnlyList materials, IReadOnlyList vertices, int numLods, Transform transform = null) : this(export) { - var hasCustomUvs = lod.ExtraUV.IsValueCreated; - UvCount = hasCustomUvs ? Math.Max(lod.NumTexCoords, numLods) : lod.NumTexCoords; + var hasCustomUvs = lod.ExtraUvs.Length > 0; + UvCount = hasCustomUvs ? Math.Max(lod.ExtraUvs.Length, numLods) : lod.ExtraUvs.Length + 1; IsTwoSided = lod.IsTwoSided; - Indices = new uint[lod.Indices.Value.Length]; + Indices = new uint[lod.Indices.Length]; for (int i = 0; i < Indices.Length; i++) { - Indices[i] = (uint) lod.Indices.Value[i]; + Indices[i] = lod.Indices[i]; } Materials = new Material[materials.Count]; @@ -129,11 +126,11 @@ public abstract class UModel : IRenderableModel Materials[m] = new Material(unrealMaterial); else Materials[m] = new Material(); } - _vertexAttributes[(int) EAttribute.Colors].Enabled = lod.VertexColors is { Length: > 0}; + _vertexAttributes[(int) EAttribute.Colors].Enabled = lod.VertexColors is { Length: > 0 }; _vertexAttributes[(int) EAttribute.BonesId].Enabled = - _vertexAttributes[(int) EAttribute.BonesWeight].Enabled = vertices is CSkelMeshVertex[]; + _vertexAttributes[(int) EAttribute.BonesWeight].Enabled = vertices is SkinnedMeshVertex[]; - Vertices = new float[lod.NumVerts * VertexSize]; + Vertices = new float[vertices.Count * VertexSize]; for (int i = 0; i < vertices.Count; i++) { var count = 0; @@ -149,18 +146,18 @@ public abstract class UModel : IRenderableModel Vertices[baseIndex + count++] = vert.Tangent.X; Vertices[baseIndex + count++] = vert.Tangent.Z; Vertices[baseIndex + count++] = vert.Tangent.Y; - Vertices[baseIndex + count++] = vert.UV.U; - Vertices[baseIndex + count++] = vert.UV.V; - Vertices[baseIndex + count++] = hasCustomUvs ? lod.ExtraUV.Value[0][i].U - 1 : .5f; + Vertices[baseIndex + count++] = vert.Uv.U; + Vertices[baseIndex + count++] = vert.Uv.V; + Vertices[baseIndex + count++] = hasCustomUvs ? lod.ExtraUvs[0][i].U - 1 : .5f; if (HasVertexColors) { - Vertices[baseIndex + count++] = lod.VertexColors[i].ToPackedARGB(); + Vertices[baseIndex + count++] = lod.VertexColors![0].Colors[i].ToPackedARGB(); } - if (vert is CSkelMeshVertex skelVert) + if (vert is SkinnedMeshVertex skelVert) { - int max = skelVert.Influences.Count; + int max = skelVert.Influences.Length; for (int j = 0; j < 8; j++) { var boneID = j < max ? skelVert.Influences[j].Bone : (ushort) 0; @@ -172,10 +169,10 @@ public abstract class UModel : IRenderableModel } } - Sections = new Section[lod.Sections.Value.Length]; + Sections = new Section[lod.Sections.Length]; for (var s = 0; s < Sections.Length; s++) { - var section = lod.Sections.Value[s]; + var section = lod.Sections[s]; Sections[s] = new Section(section.MaterialIndex, section.NumFaces * 3, section.FirstIndex); if (section.IsValid) Sections[s].SetupMaterial(Materials[section.MaterialIndex]); } @@ -392,10 +389,9 @@ public abstract class UModel : IRenderableModel return socket.Transform.LocalMatrix * socketRelation; } - public bool Save(out string label, out string savedFilePath) + public void AddToExportSession(ExportSession session) { - var toSave = new Exporter(_export, UserSettings.Default.ExportOptions); - return toSave.TryWriteToDir(new DirectoryInfo(UserSettings.Default.ModelDirectory), out label, out savedFilePath); + session.Add(_export); } public virtual void Dispose() diff --git a/FModel/Views/Snooper/Options.cs b/FModel/Views/Snooper/Options.cs index b0d21e64..916c943c 100644 --- a/FModel/Views/Snooper/Options.cs +++ b/FModel/Views/Snooper/Options.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using CUE4Parse_Conversion.Textures; using CUE4Parse.UE4.Assets.Exports.Texture; @@ -20,7 +21,7 @@ public class Options public int SelectedMorph { get; private set; } public int SelectedAnimation{ get; private set; } - public readonly Dictionary Models; + public readonly Dictionary Models; public readonly Dictionary Textures; public readonly List Lights; @@ -33,7 +34,7 @@ public class Options public Options() { - Models = new Dictionary(); + Models = new Dictionary(); Textures = new Dictionary(); Lights = new List(); @@ -110,30 +111,30 @@ public class Options public void RemoveModel(FGuid guid) { - if (!TryGetModel(guid, out var m) || m is not UModel model) + if (!TryGetModel(guid, out var m) || m is null) return; - DetachAndRemoveModels(model, true); - model.Dispose(); + DetachAndRemoveModels(m, true); + m.Dispose(); Models.Remove(guid); } - private void DetachAndRemoveModels(UModel model, bool detach) + private void DetachAndRemoveModels(IRenderableModel model, bool detach) { foreach (var socket in model.Sockets.ToList()) { foreach (var info in socket.AttachedModels) { - if (!TryGetModel(info.Guid, out var m) || m is not UModel attachedModel) + if (!TryGetModel(info.Guid, out var m) || m is null) continue; - var t = attachedModel.GetTransform(); - if (attachedModel.IsProp) + var t = m.GetTransform(); + if (m.IsProp) { - attachedModel.Attachments.SafeDetach(model, t); + m.Attachments.SafeDetach(model, t); RemoveModel(info.Guid); } - else if (detach) attachedModel.Attachments.SafeDetach(model, t); + else if (detach) m.Attachments.SafeDetach(model, t); } if (socket.IsVirtual) @@ -209,8 +210,8 @@ public class Options return texture != null; } - public bool TryGetModel(out UModel model) => Models.TryGetValue(SelectedModel, out model); - public bool TryGetModel(FGuid guid, out UModel model) => Models.TryGetValue(guid, out model); + public bool TryGetModel([MaybeNullWhen(false)] out IRenderableModel model) => Models.TryGetValue(SelectedModel, out model); + public bool TryGetModel(FGuid guid, [MaybeNullWhen(false)] out IRenderableModel model) => Models.TryGetValue(guid, out model); public bool TryGetSection(out Section section) => TryGetSection(SelectedModel, out section); public bool TryGetSection(FGuid guid, out Section section) @@ -223,7 +224,7 @@ public class Options section = null; return false; } - public bool TryGetSection(UModel model, out Section section) + public bool TryGetSection(IRenderableModel model, out Section section) { if (SelectedSection >= 0 && SelectedSection < model.Sections.Length) section = model.Sections[SelectedSection]; else section = null; diff --git a/FModel/Views/Snooper/Renderer.cs b/FModel/Views/Snooper/Renderer.cs index ab9a47b4..94e923ad 100644 --- a/FModel/Views/Snooper/Renderer.cs +++ b/FModel/Views/Snooper/Renderer.cs @@ -6,13 +6,13 @@ using System.Threading; using System.Windows; using CUE4Parse_Conversion.Animations; using CUE4Parse_Conversion.Meshes; +using CUE4Parse_Conversion.Options; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Assets.Exports.Component.SplineMesh; using CUE4Parse.UE4.Assets.Exports.Component.StaticMesh; using CUE4Parse.UE4.Assets.Exports.GeometryCollection; using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.Nanite; using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Exports.Texture; @@ -29,7 +29,6 @@ using FModel.Views.Snooper.Lights; using FModel.Views.Snooper.Models; using FModel.Views.Snooper.Shading; using OpenTK.Windowing.GraphicsLibraryFramework; -using UModel = FModel.Views.Snooper.Models.UModel; namespace FModel.Views.Snooper; @@ -175,7 +174,7 @@ public class Renderer : IDisposable t.Scale = offset.Scale3D; } - UModel addedModel = null; + IRenderableModel addedModel = null; switch (export) { case UStaticMesh st: @@ -344,7 +343,7 @@ public class Renderer : IDisposable wnd.WindowShouldClose(true, true); } - private void LoadStaticMesh(UStaticMesh original, ENaniteMeshFormat naniteFormat = ENaniteMeshFormat.OnlyNormalLODs) + private void LoadStaticMesh(UStaticMesh original, ENaniteMeshFormat naniteFormat = ENaniteMeshFormat.NoNanite) { var guid = original.LightingGuid; if (Options.TryGetModel(guid, out var model)) @@ -713,7 +712,7 @@ public class Renderer : IDisposable continue; var parameters = new CMaterialParams2(); - unrealMaterial.GetParams(parameters, EMaterialFormat.FirstLayer); + unrealMaterial.GetParams(parameters, EMaterialDepth.TopLayerOnly); if (!parameters.TryGetLinearColor(out var color, "Color")) color = FLinearColor.Gray; @@ -726,7 +725,7 @@ public class Renderer : IDisposable if (!material.TryLoad(out UMaterialInterface unrealMaterial)) continue; var parameters = new CMaterialParams2(); - unrealMaterial.GetParams(parameters, EMaterialFormat.FirstLayer); + unrealMaterial.GetParams(parameters, EMaterialDepth.TopLayerOnly); if (!byte.TryParse(material.Name.SubstringAfterLast("_"), out var indexAsByte)) indexAsByte = byte.MaxValue; diff --git a/FModel/Views/Snooper/Shading/Material.cs b/FModel/Views/Snooper/Shading/Material.cs index 011a166d..d250510a 100644 --- a/FModel/Views/Snooper/Shading/Material.cs +++ b/FModel/Views/Snooper/Shading/Material.cs @@ -317,7 +317,7 @@ public class Material : IDisposable } } - public bool ImGuiTextures(Dictionary icons, UModel model) + public bool ImGuiTextures(Dictionary icons, IRenderableModel model) { if (ImGui.BeginTable("material_textures", 2)) { diff --git a/FModel/Views/Snooper/SnimGui.cs b/FModel/Views/Snooper/SnimGui.cs index 27112d6f..d923945b 100644 --- a/FModel/Views/Snooper/SnimGui.cs +++ b/FModel/Views/Snooper/SnimGui.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using CUE4Parse.UE4.Objects.Core.Misc; using FModel.Framework; using ImGuiNET; @@ -38,30 +37,10 @@ public class Swap } } -public class Save -{ - public bool Value; - public string Label; - public string Path; - - public Save() - { - Reset(); - } - - public void Reset() - { - Value = false; - Label = string.Empty; - Path = string.Empty; - } -} - public class SnimGui { public readonly ImGuiController Controller; private readonly Swap _swapper = new (); - private readonly Save _saver = new (); private readonly string _renderer; private readonly string _version; private readonly float _tableWidth; @@ -96,7 +75,7 @@ public class SnimGui SectionWindow("Material Inspector", s.Renderer, DrawMaterialInspector, false); AnimationWindow("Timeline", s.Renderer, (icons, tracker, animations) => - tracker.ImGuiTimeline(s, _saver, icons, animations, _outlinerSize, Controller.FontSemiBold)); + tracker.ImGuiTimeline(s, icons, animations, _outlinerSize, Controller.FontSemiBold)); Window("World", () => DrawWorld(s), false); @@ -144,36 +123,13 @@ public class SnimGui } }); - Modal("Saved", _saver.Value, () => - { - ImGui.TextWrapped($"Successfully saved {_saver.Label}"); - ImGui.Separator(); - - var size = new Vector2(120, 0); - if (ImGui.Button("OK", size)) - { - _saver.Reset(); - ImGui.CloseCurrentPopup(); - } - - ImGui.SetItemDefaultFocus(); - ImGui.SameLine(); - - if (ImGui.Button("Show In Explorer", size)) - { - Process.Start("explorer.exe", $"/select, \"{_saver.Path.Replace('/', '\\')}\""); - - _saver.Reset(); - ImGui.CloseCurrentPopup(); - } - }); + ExportModal.Instance.Draw(); } private void DrawWorld(Snooper s) { if (ImGui.BeginTable("world_details", 2, ImGuiTableFlags.SizingStretchProp)) { - var b = false; var length = s.Renderer.Options.Models.Count; NoFramePaddingOnY(() => @@ -184,31 +140,7 @@ public class SnimGui if (ImGui.SmallButton("Save All")) { - foreach (var model in s.Renderer.Options.Models.Values) - { - b |= model.Save(out _, out _); - } - } - }); - - Modal("Saved", b, () => - { - ImGui.TextWrapped($"Successfully saved {length} models"); - ImGui.Separator(); - - var size = new Vector2(120, 0); - if (ImGui.Button("OK", size)) - { - ImGui.CloseCurrentPopup(); - } - - ImGui.SetItemDefaultFocus(); - ImGui.SameLine(); - - if (ImGui.Button("Show In Explorer", size)) - { - Process.Start("explorer.exe", $"/select, \"{UserSettings.Default.ModelDirectory.Replace('/', '\\')}\""); - ImGui.CloseCurrentPopup(); + ExportModal.Instance.Export(s.Renderer.Options.Models.Values, UserSettings.Default.ModelDirectory, UserSettings.GetExportOptions()); } }); @@ -412,7 +344,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio if (ImGui.MenuItem("Save")) { s.WindowShouldFreeze(true); - _saver.Value = model.Save(out _saver.Label, out _saver.Path); + ExportModal.Instance.Export([model], UserSettings.Default.ModelDirectory, UserSettings.GetExportOptions()); s.WindowShouldFreeze(false); } if (ImGui.MenuItem("Animate", model is SkeletalModel)) @@ -591,7 +523,11 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio { ImGui.PushID(0); ImGui.BeginDisabled(model.TransformsCount < 2); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - ImGui.SliderInt("", ref model.SelectedInstance, 0, model.TransformsCount - 1, "Instance %i", ImGuiSliderFlags.AlwaysClamp); + var instance = model.SelectedInstance; + if (ImGui.SliderInt("", ref instance, 0, model.TransformsCount - 1, "Instance %i", ImGuiSliderFlags.AlwaysClamp)) + { + model.SelectedInstance = instance; + } ImGui.EndDisabled(); ImGui.PopID(); if (ImGui.BeginTable("guizmo_controls", 2, ImGuiTableFlags.SizingStretchProp)) @@ -669,7 +605,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio ImGui.PopStyleVar(); } - private void DrawMaterialInspector(Dictionary icons, UModel model, Section section) + private void DrawMaterialInspector(Dictionary icons, IRenderableModel model, Section section) { var material = model.Materials[section.MaterialIndex]; @@ -906,7 +842,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio ImGui.End(); } - private void MeshWindow(string name, Renderer renderer, Action, UModel> content, bool styled = true) + private void MeshWindow(string name, Renderer renderer, Action, IRenderableModel> content, bool styled = true) { Window(name, () => { @@ -915,7 +851,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio }, styled); } - private void SectionWindow(string name, Renderer renderer, Action, UModel, Section> content, bool styled = true) + private void SectionWindow(string name, Renderer renderer, Action, IRenderableModel, Section> content, bool styled = true) { MeshWindow(name, renderer, (icons, model) => {