#nullable enable
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using IFSEngine.Generation;
using IFSEngine.Model;
using IFSEngine.Rendering;
using IFSEngine.Serialization;
using WpfDisplay.Helper;
using WpfDisplay.Properties;
using WpfDisplay.Serialization;
namespace WpfDisplay.Models;
/// <summary>
/// The main workspace model that contains a <see cref="RendererGL"/>
/// and an <see cref="IFSEngine.Model.IFS"/> that it is rendering.
/// </summary>
public partial class Workspace : ObservableObject
{
private readonly IFSHistoryTracker _tracker = new();
private List<string> _includeSources = [];
private List<TransformPlugin> _loadedTransforms = [];
private List<EffectPlugin> _loadedEffects = [];
public event EventHandler<string>? StatusTextChanged;
public event EventHandler? LoadedParamsChanged;
public event EventHandler<int>? ParamSourceRemoved;
public IReadOnlyCollection<TransformPlugin> LoadedTransforms => _loadedTransforms;
public IReadOnlyCollection<EffectPlugin> LoadedEffects => _loadedEffects;
public IReadOnlyCollection<string> IncludeSources => _includeSources;
public IReadOnlyDictionary<string, int[]> ResolutionPresets { get; private set; } = new Dictionary<string, int[]>();
public IReadOnlyDictionary<string, string> FfmpegPresets { get; private set; } = new Dictionary<string, string>();
public Author CurrentUser { get; set; } = Author.Unknown;
public bool TransparentBackground { get; set; } = false;
public bool IsFinalRenderingMode = false;
public bool InvertAxisY;
public double Sensitivity;
public bool UseWhiteForBlankParams;
public bool IsRawFrameExportEnabled { get; private set; }
public bool IsExportVideoFileEnabled { get; private set; }
public string? FfmpegPath { get; private set; }
public string? FfmpegArgs { get; private set; }
public bool SaveMetadata { get; private set; }
public bool IncludeParamsInMetadata { get; private set; }
public string? EditedFilePath { get; private set; }
public static IReadOnlyList<string> RecentFilePaths
{
get
{
var paths = new string[Settings.Default.RecentFiles.Count];
Settings.Default.RecentFiles.CopyTo(paths, 0);
return paths;
}
}
[ObservableProperty]
public partial bool HasUnsavedChanges { get; set; }
private RendererGL _renderer = null!;
public RendererGL Renderer
{
get => _renderer;
set
{
SetProperty(ref _renderer, value);
if (Ifs != null)
_renderer.LoadParams(Ifs);
}
}
public IFS Ifs { get; private set; } = null!;
public bool IsHistoryUndoable => _tracker.IsHistoryUndoable;
public bool IsHistoryRedoable => _tracker.IsHistoryRedoable;
public static string[] TemplateFilePaths => Directory.GetFiles(App.TemplatesDirectoryPath, "*.ifsjson");
/// <summary>
/// Call <see cref="Initialize"/> before using
/// </summary>
/// <param name="r"></param>
public Workspace(RendererGL r)
{
Renderer = r;
}
public async Task Initialize()
{
await ApplyUserSettings();
await LoadLibrary();
await Renderer.Initialize(_includeSources, _loadedTransforms, _loadedEffects);
Ifs = new IFS();
Renderer.LoadParams(Ifs);
}
private async Task LoadLibrary()
{
await LoadSourceLibrary();
ResolutionPresets = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, int[]>>(
await File.ReadAllTextAsync(Path.Combine(App.LibraryDirectoryPath, "ResolutionPresets.json"))) ?? [];
FfmpegPresets = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(
await File.ReadAllTextAsync(Path.Combine(App.LibraryDirectoryPath, "FfmpegPresets.json"))) ?? [];
}
private async Task LoadSourceLibrary()
{
//load includes
var includeFiles = Directory.GetFiles(App.IncludesDirectoryPath);
var loadIncludeTasks = includeFiles.Select(file => File.ReadAllTextAsync(file));
_includeSources = (await Task.WhenAll(loadIncludeTasks)).ToList();
var includeNames = includeFiles.Select(i => Path.GetFileName(i)).ToList();
//load transform plugins
var loadTransformTasks = Directory
.GetFiles(App.TransformsDirectoryPath, "*.ifstf", SearchOption.AllDirectories)
.Select(file => TransformPlugin.FromFile(file));
var allTransforms = (await Task.WhenAll(loadTransformTasks)).ToList();
//skip transforms with missing includes
var skippedTransforms = allTransforms.Where(t => t.IncludeUses.Any(u => !includeNames.Contains(u)));
_loadedTransforms = allTransforms.Except(skippedTransforms).ToList();
OnPropertyChanged(nameof(LoadedTransforms));
//load postfx plugins
var loadPostFxTasks = Directory
.GetFiles(App.PostEffectsDirectoryPath, "*.ifsfx", SearchOption.AllDirectories)
.Select(file => EffectPlugin.FromFile(file));
var allPostFxs = (await Task.WhenAll(loadPostFxTasks)).ToList();
//skip postfx with missing includes
var skippedPostFxs = allPostFxs.Where(t => t.IncludeUses.Any(u => !includeNames.Contains(u)));
_loadedEffects = allPostFxs.Except(skippedPostFxs).ToList();
OnPropertyChanged(nameof(LoadedEffects));
}
public async Task ReloadPlugins()
{
await LoadSourceLibrary();
Ifs.ReloadPlugins(LoadedTransforms, LoadedEffects);
await Renderer.LoadPlugins(_includeSources, LoadedTransforms, LoadedEffects);
Renderer.StartRenderLoop();
OnPropertyChanged(nameof(LoadedTransforms));
OnPropertyChanged(nameof(LoadedEffects));
}
public void LoadParams(IFS ifs, string? loadedFilePath)
{
TakeSnapshot();
_renderer?.LoadParams(ifs);
foreach (var i in Ifs.Iterators)//keep node positions for same iterators
ifs.Iterators.FirstOrDefault(i2 => i2.Id == i.Id)?.SetPosition(i.GetPosition());
Ifs = ifs;
SetEditedFilePath(loadedFilePath);
HasUnsavedChanges = false;
if (!Renderer.IsRendering)
Renderer.StartRenderLoop();
LoadedParamsChanged?.Invoke(this, EventArgs.Empty);
OnPropertyChanged(nameof(Ifs));
}
/// <param name="isTemplate">Whether to consider the loaded file as a template so it cannot be saved over. Pass false to consider it being edited.</param>
public async Task LoadParamsFileAsync(string path, bool isTemplate)
{
IFS ifs;
try
{
ifs = await IfsNodesSerializer.LoadJsonFileAsync(path, LoadedTransforms, LoadedEffects, false);
}
catch (System.Runtime.Serialization.SerializationException ex)
when (ex.InnerException is AggregateException exs)
{
var unknownPluginNames = exs.InnerExceptions.Select(e => ((UnknownPluginException)e).PluginName);
if (unknownPluginNames.All(pluginName => LoadedTransforms.Any(t => t.Name == pluginName) || LoadedEffects.Any(t => t.Name == pluginName)))
{
ifs = await IfsNodesSerializer.LoadJsonFileAsync(path, LoadedTransforms, LoadedEffects, true);
System.Windows.MessageBox.Show($"The loaded file was created using different versions of the following plugins:\r\n{string.Join(", ", unknownPluginNames)}.", "Warning");
}
else
throw;
}
LoadParams(ifs, isTemplate ? null : path);
}
public void RaiseAnimationFrameChanged() => OnPropertyChanged("AnimationFrame");
public void LoadBlankParams()
{
var blankParams = new IFS();
if (!UseWhiteForBlankParams)
blankParams.Palette = IFSEngine.Generation.IqPaletteGenerator.Generate(
new System.Numerics.Vector4(0.6f, 0.6f, 0.6f, 1f),
new System.Numerics.Vector4(0.5f, 0.5f, 0.5f, 1f),
new System.Numerics.Vector4(0.5f, 0.5f, 0.5f, 1f),
new System.Numerics.Vector4((float)Random.Shared.NextDouble(), (float)Random.Shared.NextDouble(), (float)Random.Shared.NextDouble(), 1f),
IFSEngine.Model.InterpolationMode.Mixbox,
10);
LoadParams(blankParams, null);
}
public void LoadRandomParams()
{
Generator g = new Generator(LoadedTransforms);
IFS r = g.GenerateOne(new GeneratorOptions());
r.ImageResolution = new System.Drawing.Size(1920, 1080);
LoadParams(r, null);
}
public async Task SaveParamsAsync()
{
if (EditedFilePath == null)
throw new InvalidOperationException();
await SaveParamsAsAsync(EditedFilePath);
}
public async Task SaveParamsAsAsync(string path)
{
Ifs.AddAuthor(CurrentUser);
await IfsNodesSerializer.SaveJsonFileAsync(Ifs, path);
SetEditedFilePath(path);
HasUnsavedChanges = false;
}
private void SetEditedFilePath(string? path)
{
EditedFilePath = path;
if (path != null)
{//update recent files list
var paths = Settings.Default.RecentFiles;
if (paths.Contains(path))
paths.Remove(path);
paths.Add(path);
if (paths.Count > 5)
paths.RemoveAt(0);
Settings.Default.Save();
OnPropertyChanged(nameof(RecentFilePaths));
}
OnPropertyChanged(nameof(EditedFilePath));
}
public void CopyToClipboard()
{
string jsonData = IfsNodesSerializer.SerializeJsonString(Ifs);
System.Windows.Clipboard.SetText(jsonData);
}
public void PasteFromClipboard()
{
string jsonData = System.Windows.Clipboard.GetText();
IFS ifs = IfsNodesSerializer.DeserializeJsonString(jsonData, LoadedTransforms, LoadedEffects, true);
LoadParams(ifs, null);
}
public void UndoHistory()
{
//LoadParams without taking snapshot
Ifs = _tracker.Undo(Ifs, LoadedTransforms, LoadedEffects);
_renderer?.LoadParams(Ifs);
LoadedParamsChanged?.Invoke(this, EventArgs.Empty);
OnPropertyChanged(nameof(Ifs));
}
public void RedoHistory()
{
//LoadParams without taking snapshot
Ifs = _tracker.Redo(Ifs, LoadedTransforms, LoadedEffects);
_renderer?.LoadParams(Ifs);
LoadedParamsChanged?.Invoke(this, EventArgs.Empty);
OnPropertyChanged(nameof(Ifs));
}
public void ClearHistory() => _tracker.Clear();
public void TakeSnapshot()
{
_tracker.TakeSnapshot(Ifs);
HasUnsavedChanges = true;
}
public async Task ApplyUserSettings()
{
if (!ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).HasFile)
{//migrate user settings from previous version
Settings.Default.Upgrade();
Settings.Default.Save();
}
Renderer.EnablePerceptualUpdates = Settings.Default.PerceptuallyUniformUpdates;
await Renderer.SetWorkgroupCount(Settings.Default.WorkgroupCount);
Renderer.TargetFramerate = Settings.Default.TargetFramerate;
InvertAxisY = Settings.Default.InvertAxisY;
Sensitivity = Settings.Default.Sensitivity;
UseWhiteForBlankParams = Settings.Default.UseWhiteForBlankParams;
IsRawFrameExportEnabled = Settings.Default.IsRawFrameExportEnabled;
IsExportVideoFileEnabled = Settings.Default.IsExportVideoFileEnabled;
FfmpegPath = Settings.Default.FfmpegPath;
FfmpegArgs = Settings.Default.FfmpegArgs;
SaveMetadata = Settings.Default.SaveMetadata;
IncludeParamsInMetadata = Settings.Default.IncludeParamsInMetadata;
CurrentUser = new Author
{
Name = Settings.Default.AuthorName,
Link = Settings.Default.AuthorLink
};
}
public void SetFinalResolution(int imageWidth, int imageHeight)
{
Ifs.ImageResolution = new System.Drawing.Size(imageWidth, imageHeight);
if (IsFinalRenderingMode)
Renderer.SetHistogramScale(1.0);
else
Renderer.SetHistogramScaleToDisplay();
OnPropertyChanged(nameof(Ifs.ImageResolution));
}
/// <param name="csvFilePath">Path to a CSV file where columns are the channel paths and rows are the frames.</param>
/// <returns>The number of successfully imported channels.</returns>
public async Task<int> ImportAnimationChannels(string csvFilePath)
{
var csv = (await File.ReadAllLinesAsync(csvFilePath)).Select(row => row.Split(',').Select(field => field.Trim()).ToArray()).ToArray();
var channelPaths = csv[0].ToArray();
//remove existing channels with same path
foreach (var channelPath in channelPaths)
Ifs.Dopesheet.Channels.Remove(channelPath);
//fill channels frame-by-frame
for (int row = 1; row < csv.Length; row++)
{
if (csv[row].Length != channelPaths.Length)
continue;//eof empty line or malformed
var frameTime = TimeOnly.FromTimeSpan(TimeSpan.FromSeconds((row - 1.0) / Ifs.Dopesheet.Fps));
for (int column = 0; column < channelPaths.Length; column++)
{
var field = csv[row][column];
var val = double.Parse(field, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
Ifs.Dopesheet.AddOrUpdateChannel(channelPaths[column], channelPaths[column], frameTime, val);
}
}
return channelPaths.Length;
}
public void UpdateStatusText(string statusText) => StatusTextChanged?.Invoke(this, statusText);
/// <summary>
/// TODO: temporary solution to remove animation channels.
/// </summary>
public void OnParamSourceRemoved(int sourceId) => ParamSourceRemoved?.Invoke(this, sourceId);
}