Split up model and skin importer into seperate classes and improved api

This commit is contained in:
miku-666
2024-08-07 23:16:31 +02:00
parent 5ab3e589a6
commit a1c564b612
10 changed files with 296 additions and 234 deletions

View File

@@ -155,6 +155,25 @@ namespace PckStudio.External.Format
[JsonProperty("uuid")]
internal Guid Uuid;
internal static Element CreateCube(string name, Vector2 uvOffset, Vector3 pos, Vector3 size, float inflate, bool mirror)
{
return new Element
{
Name = name,
UseBoxUv = true,
Locked = false,
Rescale = false,
Type = "cube",
Uuid = Guid.NewGuid(),
UvOffset = uvOffset,
MirrorUv = mirror,
Inflate = inflate,
From = pos,
To = pos + size
};
}
}
internal class Texture
@@ -321,6 +340,22 @@ namespace PckStudio.External.Format
[JsonProperty("textures")]
internal Texture[] Textures;
internal static BlockBenchModel Create(string name, Size textureResolution, IEnumerable<Texture> textures)
{
return new BlockBenchModel()
{
Name = name,
Textures = textures.ToArray(),
TextureResolution = textureResolution,
ModelIdentifier = "",
Metadata = new Meta()
{
FormatVersion = "4.5",
ModelFormat = "free",
UseBoxUv = true,
}
};
}
}
}

View File

@@ -181,20 +181,20 @@ namespace PckStudio.Forms.Editor
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Title = "Save Model File";
saveFileDialog.Filter = SkinModelImporter.SupportedModelFileFormatsFilter;
saveFileDialog.Filter = SkinModelImporter.Default.SupportedModelFileFormatsFilter;
saveFileDialog.FileName = _skin.MetaData.Name.TrimEnd(new char[] { '\n', '\r' }).Replace(' ', '_');
if (saveFileDialog.ShowDialog() == DialogResult.OK)
SkinModelImporter.Export(saveFileDialog.FileName, _skin.Model);
SkinModelImporter.Default.Export(saveFileDialog.FileName, _skin.Model);
}
private void importSkinButton_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Title = "Select Model File";
openFileDialog.Filter = SkinModelImporter.SupportedModelFileFormatsFilter;
openFileDialog.Filter = SkinModelImporter.Default.SupportedModelFileFormatsFilter;
if (MessageBox.Show("Import custom model project file? Your current work will be lost!", "", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1) == DialogResult.Yes && openFileDialog.ShowDialog() == DialogResult.OK)
{
SkinModelInfo modelInfo = SkinModelImporter.Import(openFileDialog.FileName);
SkinModelInfo modelInfo = SkinModelImporter.Default.Import(openFileDialog.FileName);
if (modelInfo is not null)
{
_skin.Model = modelInfo;

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PckStudio.Internal;
using PckStudio.Internal.Skin;
namespace PckStudio.Interfaces
{
internal interface ISkinModelImportProvider : IModelImportProvider<SkinModelInfo>
{
}
}

View File

@@ -46,7 +46,6 @@ namespace PckStudio.Internal.App
_ = Tiles.MoonPhaseImageList;
_ = Tiles.PaintingImageList;
SettingsManager.Initialize();
SkinModelImporter.Initialize();
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Task.Run(GetContributors);
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using OMI.Formats.Model;
using PckStudio.External.Format;
using System.Numerics;
using System.IO;
namespace PckStudio.Internal
{
internal sealed class GameModelImporter : ModelImporter<GameModelInfo>
{
public static GameModelImporter Default { get; } = new GameModelImporter();
private GameModelImporter()
{
InternalAddProvider(new FileDialogFilter("", "*.bbmodel"), null, ExportBlockBenchModel);
}
internal static void ExportBlockBenchModel(string fileName, GameModelInfo modelInfo)
{
BlockBenchModel blockBenchModel = BlockBenchModel.Create(Path.GetFileNameWithoutExtension(fileName), modelInfo.Model.TextureSize, modelInfo.Textures.Select(nt => (Texture)nt));
Dictionary<string, Outline> outliners = new Dictionary<string, Outline>(5);
List<Element> elements = new List<Element>(modelInfo.Model.Parts.Count);
Vector3 transformAxis = new Vector3(1, 1, 0);
Outline GetOrCreateOutline(string partName)
{
if (!outliners.ContainsKey(partName))
outliners.Add(partName, new Outline(partName));
return outliners[partName];
}
foreach (ModelPart part in modelInfo.Model.Parts.Values)
{
//Outline outline = GetOrCreateOutline(part.Name);
var outline = new Outline(part.Name);
Vector3 partTranslation = part.Translation;
outline.Origin = TransformSpace(partTranslation, Vector3.Zero, transformAxis);
outline.Origin += Vector3.UnitY * 24f;
Vector3 rotation = part.Rotation + part.AdditionalRotation;
outline.Rotation = rotation * TransformSpace(Vector3.One, Vector3.Zero, transformAxis);
foreach (ModelBox box in part.Boxes)
{
Element element = CreateElement(box, partTranslation, part.Name);
element.Origin = outline.Origin;
elements.Add(element);
outline.Children.Add(element.Uuid);
}
outliners.Add(part.Name, outline);
}
blockBenchModel.Elements = elements.ToArray();
blockBenchModel.Outliner = JArray.FromObject(outliners);
string content = JsonConvert.SerializeObject(blockBenchModel);
File.WriteAllText(fileName, content);
}
private static Element CreateElement(ModelBox box, Vector3 origin, string name)
{
Vector3 pos = box.Position;
Vector3 size = box.Size;
Vector3 transformPos = TransformSpace(pos + origin, size, new Vector3(1, 1, 0)) + 24f * Vector3.UnitY;
return Element.CreateCube(name, box.Uv, transformPos, size, box.Scale, box.Mirror);
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OMI.Formats.Model;
namespace PckStudio.Internal
{
internal class GameModelInfo
{
public Model Model { get; }
public IEnumerable<NamedTexture> Textures { get; }
public GameModelInfo(Model model, IEnumerable<NamedTexture> textures)
{
Model = model;
Textures = textures;
}
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using PckStudio.Interfaces;
namespace PckStudio.Internal
{
internal abstract class ModelImporter<T> where T : class
{
private Dictionary<string, IModelImportProvider<T>> _importProviders = new Dictionary<string, IModelImportProvider<T>>();
private sealed class SimpleSkinImportProvider : IModelImportProvider<T>
{
public string Name => nameof(SimpleSkinImportProvider);
public FileDialogFilter DialogFilter => _dialogFilter;
private FileDialogFilter _dialogFilter;
private Func<string, T> _import;
private Action<string, T> _export;
public SimpleSkinImportProvider(FileDialogFilter dialogFilter, Func<string, T> import, Action<string, T> export)
{
_dialogFilter = dialogFilter;
_import = import;
_export = export;
}
public void Export(string filename, T model)
{
_ = _export ?? throw new NotImplementedException();
_export(filename, model);
}
public T Import(string filename)
{
_ = _import ?? throw new NotImplementedException();
return _import(filename);
}
public void Export(ref Stream stream, T model)
{
throw new NotImplementedException();
}
public T Import(Stream stream)
{
throw new NotImplementedException();
}
}
internal string SupportedModelFileFormatsFilter => string.Join("|", _importProviders.Values.Select(p => p.DialogFilter));
public T Import(string filename)
{
if (HasProvider(filename))
return GetProvider(filename).Import(filename);
Trace.TraceWarning($"[{nameof(SkinModelImporter)}:Import] No provider found for '{Path.GetExtension(filename)}'.");
return default;
}
public void Export(string filename, T model)
{
if (model is null)
{
Trace.TraceError($"[{nameof(SkinModelImporter)}:Export] Model is null.");
return;
}
if (!HasProvider(filename))
{
string fileExtension = Path.GetExtension(filename);
Trace.TraceWarning($"[{nameof(SkinModelImporter)}:Export] No provider found for '{fileExtension}'.");
return;
}
GetProvider(filename).Export(filename, model);
}
internal bool AddProvider(IModelImportProvider<T> provider)
{
if (_importProviders.ContainsKey(provider.DialogFilter.Extension))
return false;
_importProviders.Add(provider.DialogFilter.Extension, provider);
return true;
}
protected bool HasProvider(string filename)
{
string fileExtension = Path.GetExtension(filename);
return _importProviders.ContainsKey(fileExtension) && _importProviders[fileExtension] is not null;
}
protected IModelImportProvider<T> GetProvider(string filename)
{
string fileExtension = Path.GetExtension(filename);
return _importProviders.ContainsKey(fileExtension) ? _importProviders[fileExtension] : null;
}
protected bool InternalAddProvider(FileDialogFilter dialogFilter, Func<string, T> import, Action<string, T> export)
{
if (import == null || export == null)
return false;
return AddProvider(new SimpleSkinImportProvider(dialogFilter, import, export));
}
/// <summary>
/// Translates coordinate unit system into our coordinate system
/// </summary>
/// <param name="origin">Position/Origin of the Object(Cube).</param>
/// <param name="size">The Size of the Object(Cube).</param>
/// <param name="translationUnit">Describes what axises need translation.</param>
/// <returns>The translated position</returns>
protected static Vector3 TransformSpace(Vector3 origin, Vector3 size, Vector3 translationUnit)
{
// The translation unit describes what axises need to be swapped
// Example:
// translation unit = (1, 0, 0) => This translation unit will ONLY swap the X axis
translationUnit = Vector3.Clamp(translationUnit, Vector3.Zero, Vector3.One);
// To better understand see:
// https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUKgBgAJVMA6AOQgFsBTMASwGMBnAbj1QGYT0iBhIgG88RMb3SjxI3OLlEAbgEMwRBlAAOEYEQC8RKLQDuRAGq0mwAPZguACkwwijogQCUHWfLHLVtAB4aFsC0cHoGxmbBNvYAtC7xTpgeUt6+RGC0LOEAKmBKUCwAYjbU/FY2cOpKISx26lrAKV7epACcdpkszd5i7Z1ZevoBQZahPeIAvqlEM9wkmABsUZYxRHkFxaXlldW1duartmqa2m4zMr2KKhmD+ofWtmT8ADZK1Br1p8BODzFkAC16FZftEngB5QwTbxdIgAKn06E8V1hsXuYK4ZEhtGRvVQAHYiLEurixNNcJMgA
Vector3 transformUnit = -((translationUnit * 2) - Vector3.One);
Vector3 pos = origin;
// The next line essentialy does uses the fomular below just on all axis.
// x = -(pos.x + size.x)
pos *= transformUnit;
pos -= size * translationUnit;
return pos;
}
}
}

View File

@@ -18,78 +18,35 @@
using System;
using System.IO;
using System.Linq;
using System.Drawing;
using System.Numerics;
using System.Diagnostics;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PckStudio.Extensions;
using PckStudio.External.Format;
using PckStudio.Interfaces;
using PckStudio.Properties;
using PckStudio.Internal.Skin;
using PckStudio.Internal.Json;
using PckStudio.Internal.IO.PSM;
using PckStudio.External.Format;
using PckStudio.Internal.FileFormats;
using PckStudio.Forms.Additional_Popups;
using System.Drawing;
using PckStudio.Internal.Skin;
using OMI.Formats.Model;
using PckStudio.Internal.Json;
using System.Collections.ObjectModel;
using PckStudio.Properties;
using PckStudio.Interfaces;
namespace PckStudio.Internal
{
internal static class SkinModelImporter
internal sealed class SkinModelImporter : ModelImporter<SkinModelInfo>
{
private static Dictionary<string, ISkinModelImportProvider> _importProviders = new Dictionary<string, ISkinModelImportProvider>();
private sealed class SimpleSkinImportProvider : ISkinModelImportProvider
{
public string Name => nameof(SimpleSkinImportProvider);
public FileDialogFilter DialogFilter => _dialogFilter;
private FileDialogFilter _dialogFilter;
private Func<string, SkinModelInfo> _import;
private Action<string, SkinModelInfo> _export;
public SimpleSkinImportProvider(FileDialogFilter dialogFilter, Func<string, SkinModelInfo> import, Action<string, SkinModelInfo> export)
{
_dialogFilter = dialogFilter;
_import = import;
_export = export;
}
public void Export(string filename, SkinModelInfo model)
{
_ = _export ?? throw new NotImplementedException();
_export(filename, model);
}
public SkinModelInfo Import(string filename)
{
_ = _import ?? throw new NotImplementedException();
return _import(filename);
}
public void Export(ref Stream stream, SkinModelInfo model)
{
throw new NotImplementedException();
}
public SkinModelInfo Import(Stream stream)
{
throw new NotImplementedException();
}
}
internal static string SupportedModelFileFormatsFilter => string.Join("|", _importProviders.Values.Select(p => p.DialogFilter));
public static SkinModelImporter Default { get; } = new SkinModelImporter();
internal static ReadOnlyDictionary<string, JsonModelMetaData> ModelTextureLocations { get; private set; }
internal static void Initialize()
internal SkinModelImporter()
{
ModelTextureLocations = JsonConvert.DeserializeObject<ReadOnlyDictionary<string, JsonModelMetaData>>(Resources.modelTextureLocations);
InternalAddProvider(new("Pck skin model(*.psm)", "*.psm"), ImportPsm, ExportPsm);
@@ -97,47 +54,6 @@ namespace PckStudio.Internal
InternalAddProvider(new("Bedrock (Legacy) Model(*.geo.json;*.json)", "*.geo.json;*.json"), ImportBedrockJson, ExportBedrockJson);
}
internal static bool AddProvider(ISkinModelImportProvider provider)
{
if (_importProviders.ContainsKey(provider.DialogFilter.Extension))
return false;
_importProviders.Add(provider.DialogFilter.Extension, provider);
return true;
}
private static bool InternalAddProvider(FileDialogFilter dialogFilter, Func<string, SkinModelInfo> import, Action<string, SkinModelInfo> export)
{
if (import == null || export == null)
return false;
return AddProvider(new SimpleSkinImportProvider(dialogFilter, import, export));
}
internal static SkinModelInfo Import(string fileName)
{
string fileExtension = Path.GetExtension(fileName);
if (_importProviders.ContainsKey(fileExtension) && _importProviders[fileExtension] is not null)
return _importProviders[fileExtension].Import(fileName);
Trace.TraceWarning($"[{nameof(SkinModelImporter)}:Import] No provider found for '{fileExtension}'.");
return null;
}
internal static void Export(string fileName, SkinModelInfo model)
{
if (model is null)
{
Trace.TraceError($"[{nameof(SkinModelImporter)}:Export] Model is null.");
return;
}
string fileExtension = Path.GetExtension(fileName);
if (_importProviders.ContainsKey(fileExtension) && _importProviders[fileExtension] is not null)
_importProviders[fileExtension].Export(fileName, model);
Trace.TraceWarning($"[{nameof(SkinModelImporter)}:Export] No provider found for '{fileExtension}'.");
}
internal static SkinModelInfo ImportPsm(string fileName)
{
var reader = new PSMFileReader();
@@ -250,7 +166,7 @@ namespace PckStudio.Internal
internal static void ExportBlockBenchModel(string fileName, SkinModelInfo modelInfo)
{
Image exportTexture = FixTexture(modelInfo);
BlockBenchModel blockBenchModel = CreateBlockBenchModel(Path.GetFileNameWithoutExtension(fileName), new Size(64, exportTexture.Width == exportTexture.Height ? 64 : 32), [exportTexture]);
BlockBenchModel blockBenchModel = BlockBenchModel.Create(Path.GetFileNameWithoutExtension(fileName), new Size(64, exportTexture.Width == exportTexture.Height ? 64 : 32), [exportTexture]);
Dictionary<string, Outline> outliners = new Dictionary<string, Outline>(5);
List<Element> elements = new List<Element>(modelInfo.AdditionalBoxes.Count);
@@ -290,69 +206,6 @@ namespace PckStudio.Internal
File.WriteAllText(fileName, content);
}
internal static void ExportBlockBenchModel(string fileName, Model model, IEnumerable<NamedTexture> textures)
{
BlockBenchModel blockBenchModel = CreateBlockBenchModel(Path.GetFileNameWithoutExtension(fileName), model.TextureSize, textures.Select(nt => (Texture)nt));
Dictionary<string, Outline> outliners = new Dictionary<string, Outline>(5);
List<Element> elements = new List<Element>(model.Parts.Count);
Vector3 transformAxis = new Vector3(1, 1, 0);
Outline GetOrCreateOutline(string partName)
{
if (!outliners.ContainsKey(partName))
outliners.Add(partName, new Outline(partName));
return outliners[partName];
}
foreach (ModelPart part in model.Parts.Values)
{
//Outline outline = GetOrCreateOutline(part.Name);
var outline = new Outline(part.Name);
Vector3 partTranslation = part.Translation;
outline.Origin = TransformSpace(partTranslation, Vector3.Zero, transformAxis);
outline.Origin += Vector3.UnitY * 24f;
Vector3 rotation = part.Rotation + part.AdditionalRotation;
outline.Rotation = rotation * TransformSpace(Vector3.One, Vector3.Zero, transformAxis);
foreach (ModelBox box in part.Boxes)
{
Element element = CreateElement(box, partTranslation, part.Name);
element.Origin = outline.Origin;
elements.Add(element);
outline.Children.Add(element.Uuid);
}
outliners.Add(part.Name, outline);
}
blockBenchModel.Elements = elements.ToArray();
blockBenchModel.Outliner = JArray.FromObject(outliners);
string content = JsonConvert.SerializeObject(blockBenchModel);
File.WriteAllText(fileName, content);
}
private static BlockBenchModel CreateBlockBenchModel(string name, Size textureResolution, IEnumerable<Texture> textures)
{
return new BlockBenchModel()
{
Name = name,
Textures = textures.ToArray(),
TextureResolution = textureResolution,
ModelIdentifier = "",
Metadata = new Meta()
{
FormatVersion = "4.5",
ModelFormat = "free",
UseBoxUv = true,
}
};
}
private static Element CreateElement(SkinBOX box)
{
Vector3 transformPos = TranslateFromInternalPosistion(box, new Vector3(1, 1, 0));
@@ -362,35 +215,9 @@ namespace PckStudio.Internal
return element;
}
private static Element CreateElement(ModelBox box, Vector3 origin, string name)
{
Vector3 pos = box.Position;
Vector3 size = box.Size;
Vector3 transformPos = TranslateToInternalPosition("", pos + origin, size, new Vector3(1, 1, 0));
return CreateElement(name, box.Uv, transformPos, size, box.Scale, box.Mirror);
}
private static Element CreateElement(Vector2 uvOffset, Vector3 pos, Vector3 size, float inflate, bool mirror)
{
return CreateElement("cube", uvOffset, pos, size, inflate, mirror);
}
private static Element CreateElement(string name, Vector2 uvOffset, Vector3 pos, Vector3 size, float inflate, bool mirror)
{
return new Element
{
Name = name,
UseBoxUv = true,
Locked = false,
Rescale = false,
Type = "cube",
Uuid = Guid.NewGuid(),
UvOffset = uvOffset,
MirrorUv = mirror,
Inflate = inflate,
From = pos,
To = pos + size
};
return Element.CreateCube("cube", uvOffset, pos, size, inflate, mirror);
}
internal static SkinModelInfo ImportBedrockJson(string fileName)
@@ -698,7 +525,7 @@ namespace PckStudio.Internal
return result;
}
private static Vector3 GetOffset(string name, ref Dictionary<string, SkinPartOffset> offsetLookUp, IReadOnlyList<SkinPartOffset> partOffsets)
private static Vector3 GetOffset(string name, ref Dictionary<string, SkinPartOffset> offsetLookUp, IEnumerable<SkinPartOffset> partOffsets)
{
if (offsetLookUp.ContainsKey(name))
{
@@ -737,30 +564,5 @@ namespace PckStudio.Internal
{
return TranslateToInternalPosition(skinBox.Type, skinBox.Pos, skinBox.Size, translationUnit);
}
/// <summary>
/// Translates coordinate unit system into our coordinate system
/// </summary>
/// <param name="origin">Position/Origin of the Object(Cube).</param>
/// <param name="size">The Size of the Object(Cube).</param>
/// <param name="translationUnit">Describes what axises need translation.</param>
/// <returns>The translated position</returns>
private static Vector3 TransformSpace(Vector3 origin, Vector3 size, Vector3 translationUnit)
{
// The translation unit describes what axises need to be swapped
// Example:
// translation unit = (1, 0, 0) => This translation unit will ONLY swap the X axis
translationUnit = Vector3.Clamp(translationUnit, Vector3.Zero, Vector3.One);
// To better understand see:
// https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUKgBgAJVMA6AOQgFsBTMASwGMBnAbj1QGYT0iBhIgG88RMb3SjxI3OLlEAbgEMwRBlAAOEYEQC8RKLQDuRAGq0mwAPZguACkwwijogQCUHWfLHLVtAB4aFsC0cHoGxmbBNvYAtC7xTpgeUt6+RGC0LOEAKmBKUCwAYjbU/FY2cOpKISx26lrAKV7epACcdpkszd5i7Z1ZevoBQZahPeIAvqlEM9wkmABsUZYxRHkFxaXlldW1duartmqa2m4zMr2KKhmD+ofWtmT8ADZK1Br1p8BODzFkAC16FZftEngB5QwTbxdIgAKn06E8V1hsXuYK4ZEhtGRvVQAHYiLEurixNNcJMgA
Vector3 transformUnit = -((translationUnit * 2) - Vector3.One);
Vector3 pos = origin;
// The next line essentialy does uses the fomular below just on all axis.
// x = -(pos.x + size.x)
pos *= transformUnit;
pos -= size * translationUnit;
return pos;
}
}
}

View File

@@ -648,7 +648,7 @@ namespace PckStudio
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
SkinModelImporter.ExportBlockBenchModel(openFileDialog.FileName, model, textures);
GameModelImporter.Default.Export(openFileDialog.FileName, new GameModelInfo(model, textures));
}
}
}

View File

@@ -151,7 +151,7 @@
</Compile>
<Compile Include="Extensions\OpenTkMatrixExtensions.cs" />
<Compile Include="Interfaces\IModelImportProvider.cs" />
<Compile Include="Interfaces\ISkinModelImportProvider.cs" />
<Compile Include="Internal\GameModelInfo.cs" />
<Compile Include="Internal\IO\PSM\PSMFileReader.cs" />
<Compile Include="Internal\IO\PSM\PSMFileWriter.cs" />
<Compile Include="Internal\GameConstants.cs" />
@@ -169,6 +169,8 @@
<Compile Include="Internal\FileDialogFilter.cs" />
<Compile Include="Internal\Deserializer\ImageDeserializer.cs" />
<Compile Include="Internal\Misc\RichPresenceClient.cs" />
<Compile Include="Internal\GameModelImporter.cs" />
<Compile Include="Internal\ModelImporter.cs" />
<Compile Include="Internal\SkinModelImporter.cs" />
<Compile Include="Internal\ModelPartSpecifics.cs" />
<Compile Include="Internal\Json\JsonModelMetaData.cs" />