#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using IFSEngine.Model;
using IFSEngine.Serialization;
using IFSEngine.Utility;
using WpfDisplay.Helper;
using WpfDisplay.Models;
using WpfDisplay.Services;
namespace WpfDisplay.ViewModels;
public sealed partial class MainViewModel : ObservableObject, IAsyncDisposable
{
internal readonly Workspace workspace;
public IFS Ifs => workspace.Ifs;
public ToneMappingViewModel ToneMappingViewModel { get; }
public CameraSettingsViewModel CameraSettingsViewModel { get; }
public PerformanceViewModel PerformanceViewModel { get; }
public QualitySettingsViewModel QualitySettingsViewModel { get; }
public IFSViewModel IFSViewModel { get; }
public AnimationViewModel AnimationViewModel { get; }
public PostFxPanelViewModel PostFxPanelViewModel { get; }
public bool TransparentBackground
{
get => workspace.TransparentBackground;
set
{
workspace.TakeSnapshot();
workspace.TransparentBackground = value;
if (value)
IFSViewModel.BackgroundColor = Colors.Black;
OnPropertyChanged(nameof(TransparentBackground));
OnPropertyChanged(nameof(IsColorPickerEnabled));
}
}
[ObservableProperty]
public partial string StatusBarText { get; set; } = string.Empty;
[ObservableProperty]
public partial bool IsHintsPanelVisible { get; set; } = true;
[ObservableProperty]
public partial bool IsGamepadConnected { get; set; } = false;
public string IsRenderingIcon => workspace.Renderer.IsRendering ? "||" : "▶️";
public string IterationLevel => BitOperations.Log2(1 + workspace.Renderer.TotalIterations / (ulong)(workspace.Renderer.HistogramWidth * workspace.Renderer.HistogramHeight)).ToString("00.");
public double IterationProgressPercent
{
get
{
//this is to avoid interrupting the render thread with gui updates
Task.Delay(50).ContinueWith(t => OnPropertyChanged(nameof(IterationProgressPercent)));
return 100.0 * (workspace.Renderer.TotalIterations / (double)(workspace.Renderer.HistogramWidth * workspace.Renderer.HistogramHeight)) / (Math.Pow(2, workspace.Ifs.TargetIterationLevel) - 1);
}
}
public bool IsColorPickerEnabled => !TransparentBackground;
//Main display settings:
public bool InvertAxisY => workspace.InvertAxisY;
public float Sensitivity => (float)workspace.Sensitivity;
public string WindowTitle => workspace.Ifs is null ? "IFSRenderer" : $"{workspace.Ifs.Title}{(workspace.HasUnsavedChanges ? '*' : string.Empty)} - IFSRenderer";
public string IFSTitle
{
get => workspace.Ifs.Title;
set
{
if (workspace.Ifs.Title != value)
{
workspace.TakeSnapshot();
workspace.Ifs.Title = value;
OnPropertyChanged(nameof(IFSTitle));
OnPropertyChanged(nameof(WindowTitle));
}
}
}
public IEnumerable<Author> AuthorList => workspace.Ifs.Authors;
public static IEnumerable<string> RecentFilePaths => Workspace.RecentFilePaths.Reverse();
public static bool HasRecentFiles => RecentFilePaths.Any();
public static IReadOnlyDictionary<string, string> Templates => Workspace.TemplateFilePaths.ToDictionary(path => path, path => Path.GetFileNameWithoutExtension(path));
public MainViewModel(Workspace workspace, WelcomeWorkflow workflow)
{
this.workspace = workspace;
workspace.PropertyChanged += (s, e) =>
{
switch (e.PropertyName)
{
case nameof(this.workspace.HasUnsavedChanges): OnPropertyChanged(nameof(WindowTitle)); break;
}
};
workspace.StatusTextChanged += (s, e) => StatusBarText = e;
PerformanceViewModel = new PerformanceViewModel(workspace);
QualitySettingsViewModel = new QualitySettingsViewModel(workspace);
IFSViewModel = new IFSViewModel(workspace);
AnimationViewModel = new AnimationViewModel(workspace);
CameraSettingsViewModel = new CameraSettingsViewModel(workspace);
CameraSettingsViewModel.PropertyChanged += (s, e) => OnPropertyChanged(e.PropertyName);
ToneMappingViewModel = new ToneMappingViewModel(workspace);
ToneMappingViewModel.PropertyChanged += (s, e) => OnPropertyChanged(e.PropertyName);
PostFxPanelViewModel = new PostFxPanelViewModel(workspace);
if (workflow == WelcomeWorkflow.ShowFileDialog)
ShowLoadParamsDialogCommand?.Execute(this);
workspace.Renderer.DisplayFramebufferUpdated += (s, e) => OnPropertyChanged(nameof(IterationLevel));
workspace.UpdateStatusText($"Initialized");
}
[RelayCommand]
private async Task StartStopRendering()
{
if (workspace.Renderer.IsRendering)
{
await workspace.Renderer.StopRenderLoop();
workspace.UpdateStatusText($"Stopped rendering");
}
else
{
workspace.Renderer.StartRenderLoop();
workspace.UpdateStatusText($"Started rendering");
}
OnPropertyChanged(nameof(IsRenderingIcon));
}
[RelayCommand]
private void New()
{
workspace.LoadBlankParams();
workspace.UpdateStatusText($"Blank parameters loaded");
OnPropertyChanged(nameof(IsRenderingIcon));
}
[RelayCommand]
private void LoadRandom()
{
workspace.LoadRandomParams();
workspace.UpdateStatusText($"Randomly generated parameters loaded");
OnPropertyChanged(nameof(IsRenderingIcon));
}
[RelayCommand]
private async Task SaveParams()
{
if (workspace.EditedFilePath is not null)
{
await workspace.SaveParamsAsync();
workspace.UpdateStatusText($"Parameters saved to {workspace.EditedFilePath}");
}
else
await SaveParamsAs();
}
[RelayCommand]
private async Task SaveParamsAs()
{
if (DialogHelper.ShowSaveParamsDialog(workspace.Ifs.Title, out string path))
{
if (IFSTitle == "Untitled")//Set the file name as title
IFSTitle = Path.GetFileNameWithoutExtension(path);
try
{
await workspace.SaveParamsAsAsync(path);
workspace.UpdateStatusText($"Parameters saved to {path}");
}
catch (Exception)
{
workspace.UpdateStatusText($"ERROR - Failed to save params.");
}
}
}
[RelayCommand]
private async Task ShowLoadParamsDialog()
{
if (DialogHelper.ShowOpenParamsDialog(out string path))
{
await LoadParamsFromFile(path, false);
}
}
[RelayCommand]
private void CopyClipboardParams()
{
workspace.CopyToClipboard();
workspace.UpdateStatusText("Parameters copied to Clipboard");
}
[RelayCommand]
private void PasteClipboardParams()
{
try
{
workspace.PasteFromClipboard();
workspace.UpdateStatusText("Parameters pasted from Clipboard");
}
catch (SerializationException)
{
workspace.UpdateStatusText("ERROR - Failed to paste params from Clipboard");
}
OnPropertyChanged(nameof(IsRenderingIcon));
}
/// <summary>
/// Handle file from a drag & drop operation.
/// </summary>
[RelayCommand]
private async Task DropFile(IDataObject dataObject)
{
if (FileDropHandler.IsDropSupported(dataObject, out var fileType, out var filePath))
{
switch (fileType)
{
case DroppedFile.Params:
await LoadParamsFromFile(filePath, false);
break;
case DroppedFile.Image:
var pngImge = BitmapFrame.Create(new Uri(filePath));
if (PngMetadataHelper.TryExtractParamsFromImage(pngImge, workspace.LoadedTransforms, workspace.LoadedEffects, out var ifs))
workspace.LoadParams(ifs, null);
break;
case DroppedFile.Palette:
//IFSViewModel.DropPaletteCommand.Execute(filePath);
//TODO: handle in main, open palette editor
break;
case DroppedFile.Transform:
File.Copy(filePath, Path.Combine(App.TransformsDirectoryPath, Path.GetFileName(filePath)), true);
IFSViewModel.ReloadTransformsCommand.Execute(null);
break;
}
}
}
[RelayCommand]
private async Task LoadTemplate(string path) => await LoadParamsFromFile(path, true);
private async Task LoadParamsFromFile(string path, bool isTemplate)
{
try
{
await workspace.LoadParamsFileAsync(path, isTemplate);
workspace.UpdateStatusText($"Parameters loaded from {path}");
}
catch (SerializationException ex)
when (ex.InnerException is AggregateException exs)
{//missing transforms
var unknownTransforms = exs.InnerExceptions.Select(e => (UnknownPluginException)e);
MessageBox.Show($"Failed to load params due to missing transforms:\r\n{string.Join("\r\n", unknownTransforms.Select(t => $"{t.PluginName} ({t.PluginVersion})"))}", "Error");
workspace.UpdateStatusText($"ERROR - Missing transforms: {string.Join(", ", unknownTransforms.Select(t => t.PluginName))}");
}
catch (SerializationException ex)
{
string logFilePath = App.LogException(ex);
workspace.UpdateStatusText($"ERROR - Failed to load params: {path}. See log: {logFilePath}");
}
OnPropertyChanged(nameof(IsRenderingIcon));
}
[RelayCommand]
private async Task ExportToClipboard()
{
BitmapSource bs = await workspace.Renderer.GetExportBitmapSource(TransparentBackground);
//remark: sadly, Windows Clipboard does not keep metadata, so there's no point in creating a png frame here
Clipboard.SetImage(bs);
//TODO: somehow alpha channel is not copied
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
workspace.UpdateStatusText($"Image exported to clipboard");
}
[RelayCommand]
private async Task SaveImage()
{
workspace.UpdateStatusText($"Exporting...");
var makeBitmapTask = workspace.Renderer.GetExportBitmapSource(TransparentBackground);
if (DialogHelper.ShowExportImageDialog(workspace.Ifs.Title, out string path))
{
//create metadata
workspace.Ifs.AddAuthor(workspace.CurrentUser);//make sure to include current user even when params haven't been saved yet.
BitmapMetadata? metadata = null;
if (workspace.SaveMetadata)
metadata = PngMetadataHelper.CreateMetadataFromParams(workspace.Ifs, includeSerializedParams: workspace.IncludeParamsInMetadata);
//create frame
BitmapSource bs = await makeBitmapTask;
var frame = BitmapFrame.Create(bs, null, metadata, null);
//save png
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(frame);
using (FileStream stream = new FileStream(path, FileMode.Create))
encoder.Save(stream);
//open the image for viewing. TODO: optional..
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
workspace.UpdateStatusText($"Image exported to {path}");
}
else
workspace.UpdateStatusText(string.Empty);
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
}
[RelayCommand]
private async Task SaveExr()
{
workspace.UpdateStatusText($"Exporting...");
Task<float[][][]> getDataTask = Task.Run(workspace.Renderer.ReadHistogramData);
if (DialogHelper.ShowExportExrDialog(workspace.Ifs.Title, out string path))
{
var histogramData = await getDataTask;
using var fstream = File.Create(path);
OpenEXR.WriteStream(fstream, histogramData);
workspace.UpdateStatusText($"Image exported to {path}");
}
else
workspace.UpdateStatusText(string.Empty);
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
}
/// <summary>
/// Used by InteractiveDisplay
/// </summary>
[RelayCommand]
private void TakeSnapshot()
{
IsHintsPanelVisible = false;
workspace.TakeSnapshot();
}
[RelayCommand]
private void InteractionFinished() => CameraSettingsViewModel.RaiseCameraParamsChanged();
[RelayCommand]
private static void ExitApplication() => Application.Current.MainWindow.Close();
public async ValueTask DisposeAsync() => await workspace.Renderer.DisposeAsync();
[RelayCommand]
private static void VisitIssues()
{
//Open the Issues page in user's default browser
string link = "https://github.com/bezo97/IFSRenderer/issues/";
Process.Start(new ProcessStartInfo(link) { UseShellExecute = true });
}
[RelayCommand]
private static void VisitForum()
{
//Open the Discussions page in user's default browser
string link = "https://github.com/bezo97/IFSRenderer/discussions";
Process.Start(new ProcessStartInfo(link) { UseShellExecute = true });
}
[RelayCommand]
private static void ReportBug()
{
//Open the bug report template in user's default browser
string link = "https://github.com/bezo97/IFSRenderer/issues/new?assignees=&labels=&template=bug_report.md";
Process.Start(new ProcessStartInfo(link) { UseShellExecute = true });
}
[RelayCommand]
private static void CheckUpdates()
{
//Open the Releases page in user's default browser
string link = "https://github.com/bezo97/IFSRenderer/releases";
Process.Start(new ProcessStartInfo(link) { UseShellExecute = true });
}
[RelayCommand]
private static void VisitWiki()
{
//Open the Wiki page in user's default browser
string link = "https://github.com/bezo97/IFSRenderer/wiki";
Process.Start(new ProcessStartInfo(link) { UseShellExecute = true });
}
}