diff --git a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs index db63c2a..7ce5aff 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs @@ -102,6 +102,9 @@ public sealed class IpcCallerPenumbra : IpcServiceBase public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) => _redraw.RedrawAsync(logger, handler, applicationId, token); + public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType) + => _redraw.RequestImmediateRedraw(objectIndex, redrawType); + public Task ConvertTextureFiles(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token, bool requestRedraw = true) => _textures.ConvertTextureFilesAsync(logger, jobs, progress, token, requestRedraw); diff --git a/LightlessSync/LightlessConfiguration/Configurations/ModelDecimationSettings.cs b/LightlessSync/LightlessConfiguration/Configurations/ModelDecimationSettings.cs new file mode 100644 index 0000000..eb910f0 --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/ModelDecimationSettings.cs @@ -0,0 +1,156 @@ +namespace LightlessSync.LightlessConfiguration.Configurations; + +public static class ModelDecimationDefaults +{ + public const bool EnableAutoDecimation = false; + public const int TriangleThreshold = 15_000; + public const double TargetRatio = 0.8; + public const bool NormalizeTangents = true; + public const bool AvoidBodyIntersection = true; + + /// Default triangle threshold for batch decimation (0 = no threshold). + public const int BatchTriangleThreshold = 0; + + /// Default target triangle ratio for batch decimation. + public const double BatchTargetRatio = 0.8; + + /// Default tangent normalization toggle for batch decimation. + public const bool BatchNormalizeTangents = true; + + /// Default body collision guard toggle for batch decimation. + public const bool BatchAvoidBodyIntersection = true; + + /// Default display for the batch decimation warning overlay. + public const bool ShowBatchDecimationWarning = true; + + public const bool KeepOriginalModelFiles = true; + public const bool SkipPreferredPairs = true; + public const bool AllowBody = false; + public const bool AllowFaceHead = false; + public const bool AllowTail = false; + public const bool AllowClothing = true; + public const bool AllowAccessories = true; +} + +public sealed class ModelDecimationAdvancedSettings +{ + /// Minimum triangles per connected component before skipping decimation. + public const int DefaultMinComponentTriangles = 6; + + /// Average-edge multiplier used to cap collapses. + public const float DefaultMaxCollapseEdgeLengthFactor = 1.25f; + + /// Maximum normal deviation (degrees) allowed for a collapse. + public const float DefaultNormalSimilarityThresholdDegrees = 60f; + + /// Minimum bone-weight overlap required to allow a collapse. + public const float DefaultBoneWeightSimilarityThreshold = 0.85f; + + /// UV similarity threshold to protect seams. + public const float DefaultUvSimilarityThreshold = 0.02f; + + /// UV seam cosine threshold for blocking seam collapses. + public const float DefaultUvSeamAngleCos = 0.99f; + + /// Whether to block UV seam vertices from collapsing. + public const bool DefaultBlockUvSeamVertices = true; + + /// Whether to allow collapses on boundary edges. + public const bool DefaultAllowBoundaryCollapses = false; + + /// Body collision distance factor for the primary pass. + public const float DefaultBodyCollisionDistanceFactor = 0.75f; + + /// Body collision distance factor for the relaxed fallback pass. + public const float DefaultBodyCollisionNoOpDistanceFactor = 0.25f; + + /// Relax multiplier applied when the mesh is close to the body. + public const float DefaultBodyCollisionAdaptiveRelaxFactor = 1.0f; + + /// Ratio of near-body vertices required to trigger relaxation. + public const float DefaultBodyCollisionAdaptiveNearRatio = 0.4f; + + /// UV threshold for relaxed body-collision mode. + public const float DefaultBodyCollisionAdaptiveUvThreshold = 0.08f; + + /// UV seam cosine threshold for relaxed body-collision mode. + public const float DefaultBodyCollisionNoOpUvSeamAngleCos = 0.98f; + + /// Expansion factor for protected vertices near the body. + public const float DefaultBodyCollisionProtectionFactor = 1.5f; + + /// Minimum ratio used when decimating the body proxy. + public const float DefaultBodyProxyTargetRatioMin = 0.85f; + + /// Inflation applied to body collision distances. + public const float DefaultBodyCollisionProxyInflate = 0.0005f; + + /// Body collision penetration factor used during collapse checks. + public const float DefaultBodyCollisionPenetrationFactor = 0.75f; + + /// Minimum body collision distance threshold. + public const float DefaultMinBodyCollisionDistance = 0.0001f; + + /// Minimum cell size for body collision spatial hashing. + public const float DefaultMinBodyCollisionCellSize = 0.0001f; + + /// Minimum triangles per connected component before skipping decimation. + public int MinComponentTriangles { get; set; } = DefaultMinComponentTriangles; + + /// Average-edge multiplier used to cap collapses. + public float MaxCollapseEdgeLengthFactor { get; set; } = DefaultMaxCollapseEdgeLengthFactor; + + /// Maximum normal deviation (degrees) allowed for a collapse. + public float NormalSimilarityThresholdDegrees { get; set; } = DefaultNormalSimilarityThresholdDegrees; + + /// Minimum bone-weight overlap required to allow a collapse. + public float BoneWeightSimilarityThreshold { get; set; } = DefaultBoneWeightSimilarityThreshold; + + /// UV similarity threshold to protect seams. + public float UvSimilarityThreshold { get; set; } = DefaultUvSimilarityThreshold; + + /// UV seam cosine threshold for blocking seam collapses. + public float UvSeamAngleCos { get; set; } = DefaultUvSeamAngleCos; + + /// Whether to block UV seam vertices from collapsing. + public bool BlockUvSeamVertices { get; set; } = DefaultBlockUvSeamVertices; + + /// Whether to allow collapses on boundary edges. + public bool AllowBoundaryCollapses { get; set; } = DefaultAllowBoundaryCollapses; + + /// Body collision distance factor for the primary pass. + public float BodyCollisionDistanceFactor { get; set; } = DefaultBodyCollisionDistanceFactor; + + /// Body collision distance factor for the relaxed fallback pass. + public float BodyCollisionNoOpDistanceFactor { get; set; } = DefaultBodyCollisionNoOpDistanceFactor; + + /// Relax multiplier applied when the mesh is close to the body. + public float BodyCollisionAdaptiveRelaxFactor { get; set; } = DefaultBodyCollisionAdaptiveRelaxFactor; + + /// Ratio of near-body vertices required to trigger relaxation. + public float BodyCollisionAdaptiveNearRatio { get; set; } = DefaultBodyCollisionAdaptiveNearRatio; + + /// UV threshold for relaxed body-collision mode. + public float BodyCollisionAdaptiveUvThreshold { get; set; } = DefaultBodyCollisionAdaptiveUvThreshold; + + /// UV seam cosine threshold for relaxed body-collision mode. + public float BodyCollisionNoOpUvSeamAngleCos { get; set; } = DefaultBodyCollisionNoOpUvSeamAngleCos; + + /// Expansion factor for protected vertices near the body. + public float BodyCollisionProtectionFactor { get; set; } = DefaultBodyCollisionProtectionFactor; + + /// Minimum ratio used when decimating the body proxy. + public float BodyProxyTargetRatioMin { get; set; } = DefaultBodyProxyTargetRatioMin; + + /// Inflation applied to body collision distances. + public float BodyCollisionProxyInflate { get; set; } = DefaultBodyCollisionProxyInflate; + + /// Body collision penetration factor used during collapse checks. + public float BodyCollisionPenetrationFactor { get; set; } = DefaultBodyCollisionPenetrationFactor; + + /// Minimum body collision distance threshold. + public float MinBodyCollisionDistance { get; set; } = DefaultMinBodyCollisionDistance; + + /// Minimum cell size for body collision spatial hashing. + public float MinBodyCollisionCellSize { get; set; } = DefaultMinBodyCollisionCellSize; +} diff --git a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs index 599bea1..98dbc58 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs @@ -25,15 +25,22 @@ public class PlayerPerformanceConfig : ILightlessConfiguration public bool SkipUncompressedTextureCompressionMipMaps { get; set; } = false; public bool KeepOriginalTextureFiles { get; set; } = false; public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true; - public bool EnableModelDecimation { get; set; } = false; - public int ModelDecimationTriangleThreshold { get; set; } = 15_000; - public double ModelDecimationTargetRatio { get; set; } = 0.8; - public bool ModelDecimationNormalizeTangents { get; set; } = true; - public bool KeepOriginalModelFiles { get; set; } = true; - public bool SkipModelDecimationForPreferredPairs { get; set; } = true; - public bool ModelDecimationAllowBody { get; set; } = false; - public bool ModelDecimationAllowFaceHead { get; set; } = false; - public bool ModelDecimationAllowTail { get; set; } = false; - public bool ModelDecimationAllowClothing { get; set; } = true; - public bool ModelDecimationAllowAccessories { get; set; } = true; + public bool EnableModelDecimation { get; set; } = ModelDecimationDefaults.EnableAutoDecimation; + public int ModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.TriangleThreshold; + public double ModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.TargetRatio; + public bool ModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.NormalizeTangents; + public bool ModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.AvoidBodyIntersection; + public ModelDecimationAdvancedSettings ModelDecimationAdvanced { get; set; } = new(); + public int BatchModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.BatchTriangleThreshold; + public double BatchModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.BatchTargetRatio; + public bool BatchModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.BatchNormalizeTangents; + public bool BatchModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.BatchAvoidBodyIntersection; + public bool ShowBatchModelDecimationWarning { get; set; } = ModelDecimationDefaults.ShowBatchDecimationWarning; + public bool KeepOriginalModelFiles { get; set; } = ModelDecimationDefaults.KeepOriginalModelFiles; + public bool SkipModelDecimationForPreferredPairs { get; set; } = ModelDecimationDefaults.SkipPreferredPairs; + public bool ModelDecimationAllowBody { get; set; } = ModelDecimationDefaults.AllowBody; + public bool ModelDecimationAllowFaceHead { get; set; } = ModelDecimationDefaults.AllowFaceHead; + public bool ModelDecimationAllowTail { get; set; } = ModelDecimationDefaults.AllowTail; + public bool ModelDecimationAllowClothing { get; set; } = ModelDecimationDefaults.AllowClothing; + public bool ModelDecimationAllowAccessories { get; set; } = ModelDecimationDefaults.AllowAccessories; } \ No newline at end of file diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 398d96c..ae7733b 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -2404,9 +2404,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa continue; } - var preferredPath = skipDownscaleForPair - ? fileCache.ResolvedFilepath - : _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); + var preferredPath = fileCache.ResolvedFilepath; + if (!skipDownscaleForPair && gamePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) + { + preferredPath = _textureDownscaleService.GetPreferredPath(item.Hash, preferredPath); + } + + if (!skipDecimationForPair && gamePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) + { + preferredPath = _modelDecimationService.GetPreferredPath(item.Hash, preferredPath); + } outputDict[(gamePath, item.Hash)] = preferredPath; } diff --git a/LightlessSync/Services/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index 58388ae..34bb47d 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -106,7 +106,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _baseAnalysisCts.Dispose(); } - public async Task UpdateFileEntriesAsync(IEnumerable filePaths, CancellationToken token) + public async Task UpdateFileEntriesAsync(IEnumerable filePaths, CancellationToken token, bool force = false) { var normalized = new HashSet( filePaths.Where(path => !string.IsNullOrWhiteSpace(path)), @@ -115,6 +115,8 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { return; } + + var updated = false; foreach (var objectEntries in LastAnalysis.Values) { foreach (var entry in objectEntries.Values) @@ -124,9 +126,26 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable continue; } token.ThrowIfCancellationRequested(); - await entry.ComputeSizes(_fileCacheManager, token).ConfigureAwait(false); + await entry.ComputeSizes(_fileCacheManager, token, force).ConfigureAwait(false); + + if (string.Equals(entry.FileType, "mdl", StringComparison.OrdinalIgnoreCase)) + { + var sourcePath = entry.FilePaths.FirstOrDefault(path => !string.IsNullOrWhiteSpace(path)); + if (!string.IsNullOrWhiteSpace(sourcePath)) + { + entry.UpdateTriangles(_xivDataAnalyzer.RefreshTrianglesForPath(entry.Hash, sourcePath)); + } + } + + updated = true; } } + + if (updated) + { + RecalculateSummary(); + Mediator.Publish(new CharacterDataAnalyzedMessage()); + } } private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) @@ -311,6 +330,10 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable var original = new FileInfo(path).Length; var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false); + if (compressedLen <= 0 && !string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase)) + { + compressedLen = original; + } fileCacheManager.SetSizeInfo(Hash, original, compressedLen); FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen); @@ -326,6 +349,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable private Lazy? _format; public void RefreshFormat() => _format = CreateFormatValue(); + public void UpdateTriangles(long triangles) => Triangles = triangles; private Lazy CreateFormatValue() => new(() => diff --git a/LightlessSync/Services/ModelDecimation/MdlDecimator.cs b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs index c47f3f4..cbecf68 100644 --- a/LightlessSync/Services/ModelDecimation/MdlDecimator.cs +++ b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs @@ -1,19 +1,24 @@ +using LightlessSync.LightlessConfiguration.Configurations; using Lumina.Data.Parsing; using Lumina.Extensions; -using MeshDecimator; -using MeshDecimator.Algorithms; -using MeshDecimator.Math; using Microsoft.Extensions.Logging; +using Nano = Nanomesh; using Penumbra.GameData.Files.ModelStructs; using System.Buffers.Binary; +using BoneWeight = Nanomesh.BoneWeight; using MdlFile = Penumbra.GameData.Files.MdlFile; using MsLogger = Microsoft.Extensions.Logging.ILogger; +using Vector2 = Nanomesh.Vector2F; +using Vector3 = Nanomesh.Vector3F; +using Vector3d = Nanomesh.Vector3; +using Vector4 = Nanomesh.Vector4F; namespace LightlessSync.Services.ModelDecimation; // if you're coming from another sync service, then kindly fuck off. lightless ftw lil bro internal static class MdlDecimator { private const int MaxStreams = 3; + private const int MaxUvChannels = 4; private const int ReadRetryCount = 8; private const int ReadRetryDelayMs = 250; @@ -47,10 +52,11 @@ internal static class MdlDecimator MdlFile.VertexType.UShort4, ]; - public static bool TryDecimate(string sourcePath, string destinationPath, int triangleThreshold, double targetRatio, bool normalizeTangents, MsLogger logger) + public static bool TryDecimate(string sourcePath, string destinationPath, ModelDecimationSettings settings, MsLogger logger) { try { + var tuning = settings.Advanced; if (!TryReadModelBytes(sourcePath, logger, out var data)) { logger.LogInformation("Skipping model decimation; source file locked or unreadable: {Path}", sourcePath); @@ -98,13 +104,30 @@ internal static class MdlDecimator return false; } + Dictionary bodyMeshOverrides = []; + BodyCollisionData? bodyCollision = null; + if (settings.AvoidBodyIntersection) + { + if (!TryBuildBodyCollisionData( + mdl, + lodIndex, + lodMeshStart, + lodMeshEnd, + settings, + tuning, + out bodyCollision, + out bodyMeshOverrides, + logger)) + { + bodyCollision = null; + } + } + var anyDecimated = false; var newSubMeshes = new List(mdl.SubMeshes.Length); var newVertexBuffer = new List(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0); var newIndexBuffer = new List(mdl.IndexBufferSize[lodIndex] > 0 ? (int)(mdl.IndexBufferSize[lodIndex] / sizeof(ushort)) : 0); var subMeshCursor = 0; - DecimationAlgorithm? decimationAlgorithm = null; - int? decimationUvChannelCount = null; for (var meshIndex = 0; meshIndex < meshes.Length; meshIndex++) { @@ -123,15 +146,22 @@ internal static class MdlDecimator int[] indices; bool decimated; - if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd - && TryProcessMesh(mdl, lodIndex, meshIndex, mesh, meshSubMeshes, triangleThreshold, targetRatio, normalizeTangents, + if (bodyMeshOverrides.TryGetValue(meshIndex, out var bodyOverride)) + { + updatedMesh = bodyOverride.Mesh; + updatedSubMeshes = bodyOverride.SubMeshes; + vertexStreams = bodyOverride.VertexStreams; + indices = bodyOverride.Indices; + decimated = bodyOverride.Decimated; + updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase); + } + else if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd + && TryProcessMesh(mdl, lodIndex, meshIndex, mesh, meshSubMeshes, settings, tuning, bodyCollision, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out decimated, - ref decimationAlgorithm, - ref decimationUvChannelCount, logger)) { updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase); @@ -315,16 +345,14 @@ internal static class MdlDecimator int meshIndex, MeshStruct mesh, MdlStructs.SubmeshStruct[] meshSubMeshes, - int triangleThreshold, - double targetRatio, - bool normalizeTangents, + ModelDecimationSettings settings, + ModelDecimationAdvancedSettings tuning, + BodyCollisionData? bodyCollision, out MeshStruct updatedMesh, out MdlStructs.SubmeshStruct[] updatedSubMeshes, out byte[][] vertexStreams, out int[] indices, out bool decimated, - ref DecimationAlgorithm? decimationAlgorithm, - ref int? decimationUvChannelCount, MsLogger logger) { updatedMesh = mesh; @@ -344,7 +372,7 @@ internal static class MdlDecimator } var triangleCount = (int)(mesh.IndexCount / 3); - if (triangleCount < triangleThreshold) + if (triangleCount < settings.TriangleThreshold) { return false; } @@ -361,25 +389,66 @@ internal static class MdlDecimator return false; } - var targetTriangles = (int)Math.Floor(triangleCount * targetRatio); + var targetTriangles = (int)Math.Floor(triangleCount * settings.TargetRatio); if (targetTriangles < 1 || targetTriangles >= triangleCount) { + logger.LogDebug( + "Mesh {MeshIndex} decimation target invalid ({Target} vs {Triangles})", + meshIndex, + targetTriangles, + triangleCount); return false; } - var meshDecimatorMesh = BuildMesh(decoded, subMeshIndices); - var algorithm = GetOrCreateAlgorithm(format, ref decimationAlgorithm, ref decimationUvChannelCount, logger); - algorithm.Initialize(meshDecimatorMesh); - algorithm.DecimateMesh(targetTriangles); - var decimatedMesh = algorithm.ToMesh(); + var collisionData = bodyCollision; + if (collisionData != null && IsBodyMesh(mdl, mesh)) + { + collisionData = null; + } - if (decimatedMesh.SubMeshCount != meshSubMeshes.Length) + if (!TryDecimateWithNanomesh(decoded, subMeshIndices, format, targetTriangles, tuning, collisionData, out var decimatedData, out var decimatedSubMeshIndices, out var decimationStats, out var decimationReason)) + { + logger.LogDebug("Mesh {MeshIndex} decimation failed: {Reason}", meshIndex, decimationReason); + return false; + } + + if (decimatedSubMeshIndices.Length != meshSubMeshes.Length) { logger.LogDebug("Mesh {MeshIndex} submesh count changed after decimation", meshIndex); return false; } - if (!TryEncodeMeshData(decimatedMesh, format, mesh, meshSubMeshes, normalizeTangents, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out var encodeReason)) + var decimatedTriangles = 0; + for (var subMeshIndex = 0; subMeshIndex < decimatedSubMeshIndices.Length; subMeshIndex++) + { + decimatedTriangles += decimatedSubMeshIndices[subMeshIndex].Length / 3; + } + + if (decimatedTriangles <= 0 || decimatedTriangles >= triangleCount) + { + logger.LogInformation( + "Mesh {MeshIndex} decimation produced no reduction (before {Before}, after {After}, components {Components}, eligible {Eligible}, min {Min}, max {Max}, avg {Avg:0.##}, eval {Evaluated}, collapsed {Collapsed}, reject bone {RejectBone}, body {RejectBody}, topo {RejectTopo}, invert {RejectInvert} (deg {RejectDeg}, area {RejectArea}, flip {RejectFlip})", + meshIndex, + triangleCount, + decimatedTriangles, + decimationStats.TotalComponents, + decimationStats.EligibleComponents, + decimationStats.MinTriangles, + decimationStats.MaxTriangles, + decimationStats.AvgTriangles, + decimationStats.EvaluatedEdges, + decimationStats.CollapsedEdges, + decimationStats.RejectedBoneWeights, + decimationStats.RejectedBodyCollision, + decimationStats.RejectedTopology, + decimationStats.RejectedInversion, + decimationStats.RejectedDegenerate, + decimationStats.RejectedArea, + decimationStats.RejectedFlip); + return false; + } + + if (!TryEncodeMeshData(decimatedData, decimatedSubMeshIndices, format, mesh, meshSubMeshes, settings.NormalizeTangents, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out var encodeReason)) { logger.LogDebug("Mesh {MeshIndex} encode failed: {Reason}", meshIndex, encodeReason); return false; @@ -389,70 +458,1534 @@ internal static class MdlDecimator return true; } - private static DecimationAlgorithm GetOrCreateAlgorithm( + private static bool TryDecimateWithNanomesh( + DecodedMeshData decoded, + int[][] subMeshIndices, VertexFormat format, - ref DecimationAlgorithm? decimationAlgorithm, - ref int? decimationUvChannelCount, - MsLogger logger) + int targetTriangles, + ModelDecimationAdvancedSettings tuning, + BodyCollisionData? bodyCollision, + out DecodedMeshData decimated, + out int[][] decimatedSubMeshIndices, + out ComponentStats componentStats, + out string? reason) { - var uvChannelCount = format.UvChannelCount; - if (decimationAlgorithm == null || decimationUvChannelCount != uvChannelCount) + decimated = default!; + decimatedSubMeshIndices = []; + componentStats = default; + reason = null; + + var totalTriangles = 0; + for (var i = 0; i < subMeshIndices.Length; i++) { - decimationAlgorithm = MeshDecimation.CreateAlgorithm(Algorithm.Default); - decimationAlgorithm.Logger = logger; - decimationUvChannelCount = uvChannelCount; + totalTriangles += subMeshIndices[i].Length / 3; } - return decimationAlgorithm; - } - - private static Mesh BuildMesh(DecodedMeshData decoded, int[][] subMeshIndices) - { - var mesh = new Mesh(decoded.Positions, subMeshIndices); - if (decoded.Normals != null) + if (totalTriangles <= 0) { - mesh.Normals = decoded.Normals; + reason = "No triangles to decimate."; + return false; } - if (decoded.PositionWs != null) + var targetRatio = Math.Clamp(targetTriangles / (float)totalTriangles, 0f, 1f); + var outputSubMeshes = new List[subMeshIndices.Length]; + for (var i = 0; i < outputSubMeshes.Length; i++) { - mesh.PositionWs = decoded.PositionWs; + outputSubMeshes[i] = new List(); } - if (decoded.NormalWs != null) + var positions = new List(); + var normals = format.HasNormals ? new List() : null; + var tangents = format.HasTangent1 ? new List() : null; + var tangents2 = format.HasTangent2 ? new List() : null; + var colors = format.HasColors ? new List() : null; + var boneWeights = format.HasSkinning ? new List() : null; + var positionWs = format.HasPositionW ? new List() : null; + var normalWs = format.HasNormalW ? new List() : null; + List[]? uvChannels = null; + if (format.UvChannelCount > 0) { - mesh.NormalWs = decoded.NormalWs; - } - - if (decoded.Tangents != null) - { - mesh.Tangents = decoded.Tangents; - } - - if (decoded.Tangents2 != null) - { - mesh.Tangents2 = decoded.Tangents2; - } - - if (decoded.Colors != null) - { - mesh.Colors = decoded.Colors; - } - - if (decoded.BoneWeights != null) - { - mesh.BoneWeights = decoded.BoneWeights; - } - - if (decoded.UvChannels != null) - { - for (var channel = 0; channel < decoded.UvChannels.Length; channel++) + uvChannels = new List[format.UvChannelCount]; + for (var channel = 0; channel < format.UvChannelCount; channel++) { - mesh.SetUVs(channel, decoded.UvChannels[channel]); + uvChannels[channel] = new List(); } } - return mesh; + var componentCount = 0; + var eligibleCount = 0; + var minComponentTriangles = int.MaxValue; + var maxComponentTriangles = 0; + var totalComponentTriangles = 0; + var evaluatedEdges = 0; + var collapsedEdges = 0; + var rejectedBoneWeights = 0; + var rejectedTopology = 0; + var rejectedInversion = 0; + var rejectedDegenerate = 0; + var rejectedArea = 0; + var rejectedFlip = 0; + var rejectedBodyCollision = 0; + + for (var subMeshIndex = 0; subMeshIndex < subMeshIndices.Length; subMeshIndex++) + { + var indices = subMeshIndices[subMeshIndex]; + if (indices.Length == 0) + { + continue; + } + + var components = BuildComponentsForSubMesh(indices); + foreach (var componentIndices in components) + { + if (componentIndices.Length == 0) + { + continue; + } + + var componentTriangles = componentIndices.Length / 3; + if (componentTriangles == 0) + { + continue; + } + + var componentTarget = ComputeComponentTarget(componentTriangles, targetRatio, tuning.MinComponentTriangles); + componentCount++; + totalComponentTriangles += componentTriangles; + minComponentTriangles = Math.Min(minComponentTriangles, componentTriangles); + maxComponentTriangles = Math.Max(maxComponentTriangles, componentTriangles); + if (componentTarget < componentTriangles) + { + eligibleCount++; + } + + if (!TryBuildComponentDecoded(decoded, format, componentIndices, out var componentDecoded, out var componentLocalIndices, out reason)) + { + return false; + } + + DecodedMeshData componentDecimated = componentDecoded; + var componentDecimatedIndices = componentLocalIndices; + + if (componentTarget < componentTriangles) + { + if (TryDecimateComponent(componentDecoded, format, componentLocalIndices, componentTarget, decoded.BlendWeightEncoding, tuning, bodyCollision, out var decimatedComponent, out var decimatedComponentIndices, out var decimatorStats, out _)) + { + componentDecimated = decimatedComponent; + componentDecimatedIndices = decimatedComponentIndices; + evaluatedEdges += decimatorStats.EvaluatedEdges; + collapsedEdges += decimatorStats.CollapsedEdges; + rejectedBoneWeights += decimatorStats.RejectedBoneWeights; + rejectedTopology += decimatorStats.RejectedTopology; + rejectedInversion += decimatorStats.RejectedInversion; + rejectedDegenerate += decimatorStats.RejectedDegenerate; + rejectedArea += decimatorStats.RejectedArea; + rejectedFlip += decimatorStats.RejectedFlip; + rejectedBodyCollision += decimatorStats.RejectedBodyCollision; + } + } + + if (!AppendComponentData( + componentDecimated, + componentDecimatedIndices, + format, + positions, + normals, + tangents, + tangents2, + colors, + boneWeights, + uvChannels, + positionWs, + normalWs, + outputSubMeshes[subMeshIndex], + out reason)) + { + return false; + } + } + } + + if (positions.Count > ushort.MaxValue) + { + reason = "Decimated mesh exceeds vertex limit."; + return false; + } + + componentStats = BuildComponentStats( + componentCount, + eligibleCount, + minComponentTriangles, + maxComponentTriangles, + totalComponentTriangles, + evaluatedEdges, + collapsedEdges, + rejectedBoneWeights, + rejectedTopology, + rejectedInversion, + rejectedDegenerate, + rejectedArea, + rejectedFlip, + rejectedBodyCollision); + + decimated = new DecodedMeshData( + positions.ToArray(), + normals?.ToArray(), + tangents?.ToArray(), + tangents2?.ToArray(), + colors?.ToArray(), + boneWeights?.ToArray(), + uvChannels?.Select(channel => channel.ToArray()).ToArray(), + positionWs?.ToArray(), + normalWs?.ToArray(), + decoded.BlendWeightEncoding); + + decimatedSubMeshIndices = outputSubMeshes.Select(list => list.ToArray()).ToArray(); + return true; + } + + private static ComponentStats BuildComponentStats( + int componentCount, + int eligibleCount, + int minTriangles, + int maxTriangles, + int totalTriangles, + int evaluatedEdges, + int collapsedEdges, + int rejectedBoneWeights, + int rejectedTopology, + int rejectedInversion, + int rejectedDegenerate, + int rejectedArea, + int rejectedFlip, + int rejectedBodyCollision) + { + if (componentCount <= 0) + { + return new ComponentStats(0, 0, 0, 0, 0d, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + var average = totalTriangles / (double)componentCount; + return new ComponentStats( + componentCount, + eligibleCount, + minTriangles == int.MaxValue ? 0 : minTriangles, + maxTriangles, + average, + evaluatedEdges, + collapsedEdges, + rejectedBoneWeights, + rejectedTopology, + rejectedInversion, + rejectedDegenerate, + rejectedArea, + rejectedFlip, + rejectedBodyCollision); + } + + private readonly record struct ComponentStats( + int TotalComponents, + int EligibleComponents, + int MinTriangles, + int MaxTriangles, + double AvgTriangles, + int EvaluatedEdges, + int CollapsedEdges, + int RejectedBoneWeights, + int RejectedTopology, + int RejectedInversion, + int RejectedDegenerate, + int RejectedArea, + int RejectedFlip, + int RejectedBodyCollision); + + private static int ComputeComponentTarget(int componentTriangles, float targetRatio, int minComponentTriangles) + { + var minTriangles = Math.Max(1, minComponentTriangles); + if (componentTriangles <= minTriangles) + { + return componentTriangles; + } + + var target = (int)MathF.Round(componentTriangles * targetRatio); + target = Math.Max(1, target); + return Math.Min(componentTriangles, Math.Max(minTriangles, target)); + } + + private static List BuildComponentsForSubMesh(int[] indices) + { + var components = new List(); + if (indices.Length == 0) + { + return components; + } + + var triangleCount = indices.Length / 3; + if (triangleCount <= 1) + { + components.Add(indices); + return components; + } + + var parent = new int[triangleCount]; + var rank = new byte[triangleCount]; + for (var i = 0; i < triangleCount; i++) + { + parent[i] = i; + } + + var vertexToTriangle = new Dictionary(); + for (var tri = 0; tri < triangleCount; tri++) + { + var baseIndex = tri * 3; + for (var v = 0; v < 3; v++) + { + var vertexIndex = indices[baseIndex + v]; + if (vertexToTriangle.TryGetValue(vertexIndex, out var existing)) + { + Union(parent, rank, tri, existing); + } + else + { + vertexToTriangle[vertexIndex] = tri; + } + } + } + + var componentMap = new Dictionary>(); + for (var tri = 0; tri < triangleCount; tri++) + { + var root = Find(parent, tri); + if (!componentMap.TryGetValue(root, out var list)) + { + list = []; + componentMap[root] = list; + } + + list.Add(tri); + } + + foreach (var component in componentMap.Values) + { + var slice = new int[component.Count * 3]; + var cursor = 0; + foreach (var tri in component) + { + Array.Copy(indices, tri * 3, slice, cursor, 3); + cursor += 3; + } + + components.Add(slice); + } + + return components; + } + + private static bool TryBuildComponentDecoded( + DecodedMeshData decoded, + VertexFormat format, + int[] componentIndices, + out DecodedMeshData componentDecoded, + out int[] componentLocalIndices, + out string? reason) + { + componentDecoded = default!; + componentLocalIndices = []; + reason = null; + + if (componentIndices.Length == 0) + { + reason = "Component has no indices."; + return false; + } + + var vertexMap = new Dictionary(); + var positions = new List(); + var normals = format.HasNormals ? new List() : null; + var tangents = format.HasTangent1 ? new List() : null; + var tangents2 = format.HasTangent2 ? new List() : null; + var colors = format.HasColors ? new List() : null; + var boneWeights = format.HasSkinning ? new List() : null; + var positionWs = format.HasPositionW ? new List() : null; + var normalWs = format.HasNormalW ? new List() : null; + List[]? uvChannels = null; + if (format.UvChannelCount > 0) + { + uvChannels = new List[format.UvChannelCount]; + for (var channel = 0; channel < format.UvChannelCount; channel++) + { + uvChannels[channel] = new List(); + } + } + + componentLocalIndices = new int[componentIndices.Length]; + for (var i = 0; i < componentIndices.Length; i++) + { + var globalIndex = componentIndices[i]; + if (globalIndex < 0 || globalIndex >= decoded.Positions.Length) + { + reason = "Component vertex index out of bounds."; + return false; + } + + if (!vertexMap.TryGetValue(globalIndex, out var localIndex)) + { + localIndex = positions.Count; + vertexMap[globalIndex] = localIndex; + positions.Add(decoded.Positions[globalIndex]); + + if (normals != null) + { + normals.Add(decoded.Normals != null ? decoded.Normals[globalIndex] : default); + } + if (tangents != null) + { + tangents.Add(decoded.Tangents != null ? decoded.Tangents[globalIndex] : default); + } + if (tangents2 != null) + { + tangents2.Add(decoded.Tangents2 != null ? decoded.Tangents2[globalIndex] : default); + } + if (colors != null) + { + colors.Add(decoded.Colors != null ? decoded.Colors[globalIndex] : default); + } + if (boneWeights != null) + { + boneWeights.Add(decoded.BoneWeights != null ? decoded.BoneWeights[globalIndex] : default); + } + if (positionWs != null) + { + positionWs.Add(decoded.PositionWs != null ? decoded.PositionWs[globalIndex] : 0f); + } + if (normalWs != null) + { + normalWs.Add(decoded.NormalWs != null ? decoded.NormalWs[globalIndex] : 0f); + } + if (uvChannels != null) + { + for (var channel = 0; channel < uvChannels.Length; channel++) + { + var source = decoded.UvChannels != null && channel < decoded.UvChannels.Length + ? decoded.UvChannels[channel] + : null; + uvChannels[channel].Add(source != null ? source[globalIndex] : default); + } + } + } + + componentLocalIndices[i] = localIndex; + } + + componentDecoded = new DecodedMeshData( + positions.ToArray(), + normals?.ToArray(), + tangents?.ToArray(), + tangents2?.ToArray(), + colors?.ToArray(), + boneWeights?.ToArray(), + uvChannels?.Select(channel => channel.ToArray()).ToArray(), + positionWs?.ToArray(), + normalWs?.ToArray(), + decoded.BlendWeightEncoding); + + return true; + } + + private static bool TryDecimateComponent( + DecodedMeshData componentDecoded, + VertexFormat format, + int[] componentIndices, + int targetTriangles, + BlendWeightEncoding blendWeightEncoding, + ModelDecimationAdvancedSettings tuning, + BodyCollisionData? bodyCollision, + out DecodedMeshData decimated, + out int[] decimatedIndices, + out Nano.DecimationStats decimatorStats, + out string? reason) + { + decimated = default!; + decimatedIndices = []; + decimatorStats = default; + reason = null; + + var componentTriangles = componentIndices.Length / 3; + var avgEdgeLength = ComputeAverageEdgeLength(componentDecoded.Positions, componentIndices); + bool RunDecimation( + float bodyCollisionDistanceFactor, + bool allowProtectedVertices, + bool expandProtectedVertices, + bool allowProtectedVerticesWhenRelaxed, + bool forceRelaxTopology, + bool blockUvSeamVertices, + float? uvSeamAngleCosOverride, + out DecodedMeshData runDecimated, + out int[] runDecimatedIndices, + out Nano.DecimationStats runDecimatorStats, + out string? runReason) + { + runDecimated = default!; + runDecimatedIndices = []; + runDecimatorStats = default; + runReason = null; + + if (!TryBuildNanomeshMesh(componentDecoded, [componentIndices], format, out var sharedMesh, out runReason)) + { + return false; + } + + if (avgEdgeLength > 0f && tuning.MaxCollapseEdgeLengthFactor > 0f) + { + Nano.DecimateModifier.LimitCollapseEdgeLength = true; + Nano.DecimateModifier.MaxCollapseEdgeLength = avgEdgeLength * tuning.MaxCollapseEdgeLengthFactor; + } + else + { + Nano.DecimateModifier.LimitCollapseEdgeLength = false; + Nano.DecimateModifier.MaxCollapseEdgeLength = float.PositiveInfinity; + } + + var relaxTopology = forceRelaxTopology; + var decimator = new Nano.DecimateModifier(); + if (bodyCollision != null) + { + var threshold = MathF.Max(avgEdgeLength * bodyCollisionDistanceFactor + tuning.BodyCollisionProxyInflate, tuning.MinBodyCollisionDistance); + var bodyDistanceSq = bodyCollision.ComputeDistanceSq(componentDecoded.Positions, threshold); + if (bodyDistanceSq != null) + { + var thresholdSq = threshold * threshold; + var protectionThreshold = MathF.Max(threshold * tuning.BodyCollisionProtectionFactor, threshold); + var protectionThresholdSq = protectionThreshold * protectionThreshold; + var protectedDistanceSq = allowProtectedVertices + ? bodyCollision.ComputeDistanceSq(componentDecoded.Positions, protectionThreshold) + : null; + var relaxedBodyGuard = forceRelaxTopology; + if (!forceRelaxTopology && IsNearBodyDominant(bodyDistanceSq, thresholdSq, componentDecoded.Positions.Length, tuning.BodyCollisionAdaptiveNearRatio)) + { + threshold = MathF.Max(threshold * tuning.BodyCollisionAdaptiveRelaxFactor, tuning.MinBodyCollisionDistance); + thresholdSq = threshold * threshold; + relaxedBodyGuard = true; + relaxTopology = true; + + protectionThreshold = MathF.Max(threshold * tuning.BodyCollisionProtectionFactor, threshold); + protectionThresholdSq = protectionThreshold * protectionThreshold; + if (allowProtectedVertices) + { + protectedDistanceSq = bodyCollision.ComputeDistanceSq(componentDecoded.Positions, protectionThreshold); + } + } + + decimator.SetBodyCollision(bodyDistanceSq, thresholdSq, point => bodyCollision.DistanceSq(point, thresholdSq)); + if (allowProtectedVertices && (!relaxedBodyGuard || allowProtectedVerticesWhenRelaxed)) + { + var useExpandedProtection = expandProtectedVertices && !relaxTopology; + var protectedVertices = protectedDistanceSq != null + ? BuildProtectedVertices(componentDecoded.Positions.Length, componentIndices, protectedDistanceSq, protectionThresholdSq, useExpandedProtection) + : null; + if (protectedVertices != null) + { + decimator.SetProtectedVertices(protectedVertices); + } + } + } + } + + if (relaxTopology) + { + sharedMesh.attributeDefinitions = [new Nano.AttributeDefinition(Nano.AttributeType.Normals, 0d, 0)]; + } + + var connectedMesh = sharedMesh.ToConnectedMesh(); + Nano.DecimateModifier.CollapseToEndpointsOnly = true; + var previousNormalSimilarity = Nano.DecimateModifier.NormalSimilarityThresholdDegrees; + var previousBoneWeightSimilarity = Nano.DecimateModifier.BoneWeightSimilarityThreshold; + var previousBodyPenetration = Nano.DecimateModifier.BodyCollisionPenetrationFactor; + var previousUvThreshold = Nano.DecimateModifier.UvSimilarityThreshold; + var previousAllowBoundary = Nano.DecimateModifier.AllowBoundaryCollapses; + var previousBlockUvSeamVertices = Nano.DecimateModifier.BlockUvSeamVertices; + var previousUvSeamAngleCos = Nano.DecimateModifier.UvSeamAngleCos; + try + { + Nano.DecimateModifier.NormalSimilarityThresholdDegrees = tuning.NormalSimilarityThresholdDegrees; + Nano.DecimateModifier.BoneWeightSimilarityThreshold = tuning.BoneWeightSimilarityThreshold; + Nano.DecimateModifier.BodyCollisionPenetrationFactor = tuning.BodyCollisionPenetrationFactor; + Nano.DecimateModifier.UvSimilarityThreshold = tuning.UvSimilarityThreshold; + Nano.DecimateModifier.AllowBoundaryCollapses = tuning.AllowBoundaryCollapses; + Nano.DecimateModifier.BlockUvSeamVertices = blockUvSeamVertices && tuning.BlockUvSeamVertices; + Nano.DecimateModifier.UvSeamAngleCos = tuning.UvSeamAngleCos; + + if (relaxTopology) + { + Nano.DecimateModifier.UvSimilarityThreshold = tuning.BodyCollisionAdaptiveUvThreshold; + Nano.DecimateModifier.AllowBoundaryCollapses = false; + } + + if (uvSeamAngleCosOverride.HasValue) + { + Nano.DecimateModifier.UvSeamAngleCos = uvSeamAngleCosOverride.Value; + } + + decimator.Initialize(connectedMesh); + decimator.DecimateToPolycount(targetTriangles); + runDecimatorStats = decimator.GetStats(); + } + finally + { + Nano.DecimateModifier.NormalSimilarityThresholdDegrees = previousNormalSimilarity; + Nano.DecimateModifier.BoneWeightSimilarityThreshold = previousBoneWeightSimilarity; + Nano.DecimateModifier.BodyCollisionPenetrationFactor = previousBodyPenetration; + Nano.DecimateModifier.UvSimilarityThreshold = previousUvThreshold; + Nano.DecimateModifier.AllowBoundaryCollapses = previousAllowBoundary; + Nano.DecimateModifier.BlockUvSeamVertices = previousBlockUvSeamVertices; + Nano.DecimateModifier.UvSeamAngleCos = previousUvSeamAngleCos; + } + + var decimatedShared = connectedMesh.ToSharedMesh(); + if (!TryConvertNanomeshMesh(decimatedShared, format, 1, blendWeightEncoding, out runDecimated, out var subMeshes, out runReason)) + { + return false; + } + + if (subMeshes.Length > 0) + { + runDecimatedIndices = subMeshes[0]; + } + + return true; + } + + if (!RunDecimation( + tuning.BodyCollisionDistanceFactor, + allowProtectedVertices: true, + expandProtectedVertices: true, + allowProtectedVerticesWhenRelaxed: true, + forceRelaxTopology: false, + blockUvSeamVertices: true, + uvSeamAngleCosOverride: null, + out decimated, + out decimatedIndices, + out decimatorStats, + out reason)) + { + return false; + } + + if (decimatorStats.CollapsedEdges == 0 && targetTriangles < componentTriangles && bodyCollision != null) + { + if (RunDecimation( + tuning.BodyCollisionNoOpDistanceFactor, + allowProtectedVertices: true, + expandProtectedVertices: false, + allowProtectedVerticesWhenRelaxed: true, + forceRelaxTopology: true, + blockUvSeamVertices: false, + uvSeamAngleCosOverride: tuning.BodyCollisionNoOpUvSeamAngleCos, + out var fallbackDecimated, + out var fallbackDecimatedIndices, + out var fallbackStats, + out _)) + { + var fallbackTriangles = fallbackDecimatedIndices.Length / 3; + if (fallbackStats.CollapsedEdges > 0 && fallbackTriangles > 0 && fallbackTriangles < componentTriangles) + { + decimated = fallbackDecimated; + decimatedIndices = fallbackDecimatedIndices; + decimatorStats = fallbackStats; + } + } + } + + return true; + } + + private static float ComputeAverageEdgeLength(Vector3d[] positions, int[] indices) + { + if (positions.Length == 0 || indices.Length < 3) + { + return 0f; + } + + double sum = 0d; + int count = 0; + for (var i = 0; i + 2 < indices.Length; i += 3) + { + var i0 = indices[i]; + var i1 = indices[i + 1]; + var i2 = indices[i + 2]; + if ((uint)i0 >= positions.Length || (uint)i1 >= positions.Length || (uint)i2 >= positions.Length) + { + continue; + } + + sum += Vector3d.Distance(positions[i0], positions[i1]); + sum += Vector3d.Distance(positions[i1], positions[i2]); + sum += Vector3d.Distance(positions[i2], positions[i0]); + count += 3; + } + + return count > 0 ? (float)(sum / count) : 0f; + } + + private static bool[]? BuildProtectedVertices(int vertexCount, int[] indices, float[] distanceSq, float thresholdSq, bool expand) + { + if (vertexCount <= 0 || distanceSq.Length == 0) + { + return null; + } + + var seed = new bool[vertexCount]; + var seedCount = 0; + var limit = Math.Min(vertexCount, distanceSq.Length); + for (var i = 0; i < limit; i++) + { + if (distanceSq[i] <= thresholdSq) + { + seed[i] = true; + seedCount++; + } + } + + if (seedCount == 0) + { + return null; + } + + if (!expand || indices.Length < 3) + { + return seed; + } + + var expanded = (bool[])seed.Clone(); + for (var i = 0; i + 2 < indices.Length; i += 3) + { + var a = indices[i]; + var b = indices[i + 1]; + var c = indices[i + 2]; + if ((uint)a >= vertexCount || (uint)b >= vertexCount || (uint)c >= vertexCount) + { + continue; + } + + if (seed[a] || seed[b] || seed[c]) + { + expanded[a] = true; + expanded[b] = true; + expanded[c] = true; + } + } + + return expanded; + } + + private static bool IsNearBodyDominant(float[] distanceSq, float thresholdSq, int vertexCount, float adaptiveNearRatio) + { + if (vertexCount <= 0 || distanceSq.Length == 0 || thresholdSq <= 0f) + { + return false; + } + + var limit = Math.Min(vertexCount, distanceSq.Length); + var nearCount = 0; + for (var i = 0; i < limit; i++) + { + if (distanceSq[i] <= thresholdSq) + { + nearCount++; + } + } + + return nearCount >= limit * adaptiveNearRatio; + } + + private sealed record PreprocessedMeshOutput( + MeshStruct Mesh, + MdlStructs.SubmeshStruct[] SubMeshes, + byte[][] VertexStreams, + int[] Indices, + bool Decimated); + + private static bool TryBuildBodyCollisionData( + MdlFile mdl, + int lodIndex, + int lodMeshStart, + int lodMeshEnd, + ModelDecimationSettings settings, + ModelDecimationAdvancedSettings tuning, + out BodyCollisionData? bodyCollision, + out Dictionary bodyMeshOverrides, + MsLogger logger) + { + bodyCollision = null; + bodyMeshOverrides = []; + + var meshCount = Math.Max(0, lodMeshEnd - lodMeshStart); + logger.LogInformation("Body collision: scanning {MeshCount} meshes, {MaterialCount} materials", meshCount, mdl.Materials.Length); + + if (mdl.Materials.Length == 0) + { + logger.LogInformation("Body collision: no materials found, skipping body collision."); + return false; + } + + var materialList = string.Join(", ", mdl.Materials); + logger.LogInformation("Body collision: model materials = {Materials}", materialList); + logger.LogDebug("Body collision: model materials (debug) = {Materials}", materialList); + + var proxyTargetRatio = Math.Clamp(Math.Max(settings.TargetRatio, tuning.BodyProxyTargetRatioMin), 0d, 1d); + var bodyPositions = new List(); + var bodyIndices = new List(); + var foundBody = false; + + for (var meshIndex = lodMeshStart; meshIndex < lodMeshEnd; meshIndex++) + { + var mesh = mdl.Meshes[meshIndex]; + var material = mesh.MaterialIndex < mdl.Materials.Length + ? mdl.Materials[mesh.MaterialIndex] + : "(missing material)"; + var isBody = IsBodyMaterial(material); + logger.LogInformation("Body collision: mesh {MeshIndex} material {Material} body {IsBody}", meshIndex, material, isBody); + + if (!isBody) + { + continue; + } + + foundBody = true; + var meshSubMeshes = mdl.SubMeshes + .Skip(mesh.SubMeshIndex) + .Take(mesh.SubMeshCount) + .ToArray(); + + if (!TryBuildVertexFormat(mdl.VertexDeclarations[meshIndex], out var format, out var formatReason)) + { + logger.LogDebug("Body mesh {MeshIndex} vertex format unsupported: {Reason}", meshIndex, formatReason); + continue; + } + + if (!TryDecodeMeshData(mdl, lodIndex, mesh, format, meshSubMeshes, out var decoded, out var subMeshIndices, out var decodeReason)) + { + logger.LogDebug("Body mesh {MeshIndex} decode failed: {Reason}", meshIndex, decodeReason); + continue; + } + + var triangleCount = (int)(mesh.IndexCount / 3); + var updatedMesh = mesh; + var updatedSubMeshes = CopySubMeshes(meshSubMeshes, 0, mesh.StartIndex); + var vertexStreams = CopyVertexStreams(mdl, lodIndex, mesh); + var indices = ReadIndices(mdl, lodIndex, mesh); + var decimated = false; + + var collisionDecoded = decoded; + var collisionSubMeshIndices = subMeshIndices; + + if (triangleCount >= settings.TriangleThreshold) + { + var targetTriangles = (int)Math.Floor(triangleCount * proxyTargetRatio); + if (targetTriangles >= 1 && targetTriangles < triangleCount) + { + if (TryDecimateWithNanomesh(decoded, subMeshIndices, format, targetTriangles, tuning, null, out var decimatedData, out var decimatedSubMeshIndices, out _, out var decimationReason)) + { + if (TryEncodeMeshData(decimatedData, decimatedSubMeshIndices, format, mesh, meshSubMeshes, settings.NormalizeTangents, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out var encodeReason)) + { + decimated = true; + collisionDecoded = decimatedData; + collisionSubMeshIndices = decimatedSubMeshIndices; + } + else + { + logger.LogDebug("Body mesh {MeshIndex} encode failed: {Reason}", meshIndex, encodeReason); + } + } + else + { + logger.LogDebug("Body mesh {MeshIndex} decimation failed: {Reason}", meshIndex, decimationReason); + } + } + } + + bodyMeshOverrides[meshIndex] = new PreprocessedMeshOutput(updatedMesh, updatedSubMeshes, vertexStreams, indices, decimated); + + var baseIndex = bodyPositions.Count; + bodyPositions.AddRange(collisionDecoded.Positions); + foreach (var subMesh in collisionSubMeshIndices) + { + for (var i = 0; i < subMesh.Length; i++) + { + bodyIndices.Add(subMesh[i] + baseIndex); + } + } + } + + if (!foundBody) + { + logger.LogInformation("Body collision: no body meshes matched filter."); + return false; + } + + if (bodyPositions.Count == 0 || bodyIndices.Count == 0) + { + logger.LogDebug("Body collision enabled but no body vertices were collected."); + return false; + } + + var positionArray = bodyPositions.ToArray(); + var indexArray = bodyIndices.ToArray(); + var avgEdgeLength = ComputeAverageEdgeLength(positionArray, indexArray); + var cellSize = MathF.Max(avgEdgeLength, tuning.MinBodyCollisionCellSize); + bodyCollision = new BodyCollisionData(positionArray, indexArray, cellSize, tuning.MinBodyCollisionCellSize); + return true; + } + + private static bool IsBodyMesh(MdlFile mdl, MeshStruct mesh) + { + if (mesh.MaterialIndex >= mdl.Materials.Length) + { + return false; + } + + return IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]); + } + + private static bool IsBodyMaterial(string materialPath) + { + if (string.IsNullOrWhiteSpace(materialPath)) + { + return false; + } + + var normalized = materialPath.Replace('\\', '/').ToLowerInvariant(); + var nameStart = normalized.LastIndexOf('/'); + var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized; + return fileName.Contains("_bibo", StringComparison.Ordinal) + || fileName.EndsWith("_a.mtrl", StringComparison.Ordinal); + } + + private sealed class BodyCollisionData + { + private readonly Vector3d[] _positions; + private readonly BodyTriangle[] _triangles; + private readonly Dictionary> _triangleCells; + private readonly float _cellSize; + private readonly float _cellSizeInv; + + public BodyCollisionData(Vector3d[] positions, int[] indices, float cellSize, float minCellSize) + { + _positions = positions; + _cellSize = cellSize > 0f ? cellSize : minCellSize; + _cellSizeInv = 1f / _cellSize; + + var triangles = new List(); + for (var i = 0; i + 2 < indices.Length; i += 3) + { + var a = indices[i]; + var b = indices[i + 1]; + var c = indices[i + 2]; + if ((uint)a >= _positions.Length || (uint)b >= _positions.Length || (uint)c >= _positions.Length) + { + continue; + } + + var p0 = _positions[a]; + var p1 = _positions[b]; + var p2 = _positions[c]; + var min = Vector3d.Min(p0, Vector3d.Min(p1, p2)); + var max = Vector3d.Max(p0, Vector3d.Max(p1, p2)); + triangles.Add(new BodyTriangle(a, b, c, min, max)); + } + + _triangles = triangles.ToArray(); + _triangleCells = new Dictionary>(); + + for (var triIndex = 0; triIndex < _triangles.Length; triIndex++) + { + var tri = _triangles[triIndex]; + var minCell = ToCell(tri.Min); + var maxCell = ToCell(tri.Max); + for (var x = minCell.X; x <= maxCell.X; x++) + { + for (var y = minCell.Y; y <= maxCell.Y; y++) + { + for (var z = minCell.Z; z <= maxCell.Z; z++) + { + var key = new CellKey(x, y, z); + if (!_triangleCells.TryGetValue(key, out var list)) + { + list = []; + _triangleCells[key] = list; + } + + list.Add(triIndex); + } + } + } + } + } + + public float[]? ComputeDistanceSq(Vector3d[] queryPositions, float maxDistance) + { + if (_positions.Length == 0 || queryPositions.Length == 0 || maxDistance <= 0f || _triangles.Length == 0 || _triangleCells.Count == 0) + { + return null; + } + + var result = new float[queryPositions.Length]; + var maxDistanceSq = maxDistance * maxDistance; + var radius = Math.Max(1, (int)MathF.Ceiling(maxDistance / _cellSize)); + + for (var i = 0; i < queryPositions.Length; i++) + { + var cell = ToCell(queryPositions[i]); + double minSq = double.PositiveInfinity; + var found = false; + + for (var x = -radius; x <= radius && !found; x++) + { + for (var y = -radius; y <= radius && !found; y++) + { + for (var z = -radius; z <= radius; z++) + { + var key = new CellKey(cell.X + x, cell.Y + y, cell.Z + z); + if (!_triangleCells.TryGetValue(key, out var list)) + { + continue; + } + + for (var idx = 0; idx < list.Count; idx++) + { + var tri = _triangles[list[idx]]; + var sq = PointTriangleDistanceSq(queryPositions[i], _positions[tri.A], _positions[tri.B], _positions[tri.C]); + if (sq < minSq) + { + minSq = sq; + } + + if (minSq <= maxDistanceSq) + { + found = true; + break; + } + } + } + } + } + + result[i] = minSq < double.PositiveInfinity ? (float)minSq : float.PositiveInfinity; + } + + return result; + } + + public float DistanceSq(in Vector3d point, float maxDistanceSq) + { + if (_positions.Length == 0 || _triangles.Length == 0 || _triangleCells.Count == 0) + { + return float.PositiveInfinity; + } + + if (maxDistanceSq <= 0f) + { + return float.PositiveInfinity; + } + + var maxDistance = MathF.Sqrt(maxDistanceSq); + var radius = Math.Max(1, (int)MathF.Ceiling(maxDistance / _cellSize)); + var cell = ToCell(point); + double minSq = double.PositiveInfinity; + + for (var x = -radius; x <= radius; x++) + { + for (var y = -radius; y <= radius; y++) + { + for (var z = -radius; z <= radius; z++) + { + var key = new CellKey(cell.X + x, cell.Y + y, cell.Z + z); + if (!_triangleCells.TryGetValue(key, out var list)) + { + continue; + } + + for (var idx = 0; idx < list.Count; idx++) + { + var tri = _triangles[list[idx]]; + var sq = PointTriangleDistanceSq(point, _positions[tri.A], _positions[tri.B], _positions[tri.C]); + if (sq < minSq) + { + minSq = sq; + } + + if (minSq <= maxDistanceSq) + { + return (float)minSq; + } + } + } + } + } + + return minSq < double.PositiveInfinity ? (float)minSq : float.PositiveInfinity; + } + + private CellKey ToCell(in Vector3d position) + => new( + (int)Math.Floor(position.x * _cellSizeInv), + (int)Math.Floor(position.y * _cellSizeInv), + (int)Math.Floor(position.z * _cellSizeInv)); + } + + private readonly record struct BodyTriangle(int A, int B, int C, Vector3d Min, Vector3d Max); + + private readonly record struct CellKey(int X, int Y, int Z); + + private static double PointTriangleDistanceSq(in Vector3d p, in Vector3d a, in Vector3d b, in Vector3d c) + { + var ab = b - a; + var ac = c - a; + var ap = p - a; + var d1 = Vector3d.Dot(ab, ap); + var d2 = Vector3d.Dot(ac, ap); + if (d1 <= 0d && d2 <= 0d) + { + return (p - a).LengthSquared; + } + + var bp = p - b; + var d3 = Vector3d.Dot(ab, bp); + var d4 = Vector3d.Dot(ac, bp); + if (d3 >= 0d && d4 <= d3) + { + return (p - b).LengthSquared; + } + + var vc = d1 * d4 - d3 * d2; + if (vc <= 0d && d1 >= 0d && d3 <= 0d) + { + var v = d1 / (d1 - d3); + var proj = a + ab * v; + return (p - proj).LengthSquared; + } + + var cp = p - c; + var d5 = Vector3d.Dot(ab, cp); + var d6 = Vector3d.Dot(ac, cp); + if (d6 >= 0d && d5 <= d6) + { + return (p - c).LengthSquared; + } + + var vb = d5 * d2 - d1 * d6; + if (vb <= 0d && d2 >= 0d && d6 <= 0d) + { + var w = d2 / (d2 - d6); + var proj = a + ac * w; + return (p - proj).LengthSquared; + } + + var va = d3 * d6 - d5 * d4; + if (va <= 0d && (d4 - d3) >= 0d && (d5 - d6) >= 0d) + { + var w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + var proj = b + (c - b) * w; + return (p - proj).LengthSquared; + } + + var denom = 1d / (va + vb + vc); + var v2 = vb * denom; + var w2 = vc * denom; + var projPoint = a + ab * v2 + ac * w2; + return (p - projPoint).LengthSquared; + } + + private static bool AppendComponentData( + DecodedMeshData component, + int[] componentIndices, + VertexFormat format, + List positions, + List? normals, + List? tangents, + List? tangents2, + List? colors, + List? boneWeights, + List[]? uvChannels, + List? positionWs, + List? normalWs, + List outputIndices, + out string? reason) + { + reason = null; + + if (component.Positions.Length == 0 || componentIndices.Length == 0) + { + return true; + } + + var baseIndex = positions.Count; + positions.AddRange(component.Positions); + + if (normals != null && component.Normals != null) + { + normals.AddRange(component.Normals); + } + if (tangents != null && component.Tangents != null) + { + tangents.AddRange(component.Tangents); + } + if (tangents2 != null && component.Tangents2 != null) + { + tangents2.AddRange(component.Tangents2); + } + if (colors != null && component.Colors != null) + { + colors.AddRange(component.Colors); + } + if (boneWeights != null && component.BoneWeights != null) + { + boneWeights.AddRange(component.BoneWeights); + } + if (positionWs != null && component.PositionWs != null) + { + positionWs.AddRange(component.PositionWs); + } + if (normalWs != null && component.NormalWs != null) + { + normalWs.AddRange(component.NormalWs); + } + if (uvChannels != null && component.UvChannels != null) + { + if (uvChannels.Length != component.UvChannels.Length) + { + reason = "UV channel mismatch while merging components."; + return false; + } + + for (var channel = 0; channel < uvChannels.Length; channel++) + { + uvChannels[channel].AddRange(component.UvChannels[channel]); + } + } + + for (var i = 0; i < componentIndices.Length; i++) + { + outputIndices.Add(componentIndices[i] + baseIndex); + } + + return true; + } + + private static int Find(int[] parent, int value) + { + var root = value; + while (parent[root] != root) + { + root = parent[root]; + } + + while (parent[value] != value) + { + var next = parent[value]; + parent[value] = root; + value = next; + } + + return root; + } + + private static void Union(int[] parent, byte[] rank, int a, int b) + { + var rootA = Find(parent, a); + var rootB = Find(parent, b); + if (rootA == rootB) + { + return; + } + + if (rank[rootA] < rank[rootB]) + { + parent[rootA] = rootB; + return; + } + + parent[rootB] = rootA; + if (rank[rootA] == rank[rootB]) + { + rank[rootA]++; + } + } + + private static bool TryBuildNanomeshMesh( + DecodedMeshData decoded, + int[][] subMeshIndices, + VertexFormat format, + out Nano.SharedMesh sharedMesh, + out string? reason) + { + sharedMesh = default!; + reason = null; + + var vertexCount = decoded.Positions.Length; + if (vertexCount == 0) + { + reason = "No vertices to decimate."; + return false; + } + + if (subMeshIndices.Length == 0) + { + reason = "No submesh indices."; + return false; + } + + var positions = decoded.Positions; + + var totalIndexCount = 0; + for (var i = 0; i < subMeshIndices.Length; i++) + { + totalIndexCount += subMeshIndices[i].Length; + } + + var triangles = new int[totalIndexCount]; + var groups = new Nano.Group[subMeshIndices.Length]; + var cursor = 0; + for (var i = 0; i < subMeshIndices.Length; i++) + { + var subMesh = subMeshIndices[i]; + if (subMesh.Length > 0) + { + Array.Copy(subMesh, 0, triangles, cursor, subMesh.Length); + } + groups[i] = new Nano.Group { firstIndex = cursor, indexCount = subMesh.Length }; + cursor += subMesh.Length; + } + + var flags = BuildFfxivAttributeFlags(format); + var attributes = new Nano.MetaAttributeList(vertexCount); + + for (var i = 0; i < vertexCount; i++) + { + var attr = new Nano.FfxivVertexAttribute( + flags, + format.HasNormals && decoded.Normals != null ? decoded.Normals[i] : default, + format.HasTangent1 && decoded.Tangents != null ? decoded.Tangents[i] : default, + format.HasTangent2 && decoded.Tangents2 != null ? decoded.Tangents2[i] : default, + format.UvChannelCount > 0 && decoded.UvChannels != null ? decoded.UvChannels[0][i] : default, + format.UvChannelCount > 1 && decoded.UvChannels != null ? decoded.UvChannels[1][i] : default, + format.UvChannelCount > 2 && decoded.UvChannels != null ? decoded.UvChannels[2][i] : default, + format.UvChannelCount > 3 && decoded.UvChannels != null ? decoded.UvChannels[3][i] : default, + format.HasColors && decoded.Colors != null ? decoded.Colors[i] : default, + format.HasSkinning && decoded.BoneWeights != null ? decoded.BoneWeights[i] : default, + format.HasPositionW && decoded.PositionWs != null ? decoded.PositionWs[i] : 0f, + format.HasNormalW && decoded.NormalWs != null ? decoded.NormalWs[i] : 0f); + + attributes[i] = new Nano.MetaAttribute(attr); + } + + sharedMesh = new Nano.SharedMesh + { + positions = positions, + triangles = triangles, + groups = groups, + attributes = attributes, + attributeDefinitions = [new Nano.AttributeDefinition(Nano.AttributeType.Normals, Nano.ConnectedMesh.EdgeBorderPenalty, 0)], + }; + + return true; + } + + private static bool TryConvertNanomeshMesh( + Nano.SharedMesh decimatedShared, + VertexFormat format, + int expectedSubMeshCount, + BlendWeightEncoding blendWeightEncoding, + out DecodedMeshData decimated, + out int[][] decimatedSubMeshIndices, + out string? reason) + { + decimated = default!; + decimatedSubMeshIndices = []; + reason = null; + + if (decimatedShared.triangles == null || decimatedShared.triangles.Length == 0) + { + reason = "No triangles after decimation."; + return false; + } + + var groups = decimatedShared.groups; + var triangles = decimatedShared.triangles; + int[][] subMeshIndices; + + if (groups != null && groups.Length == expectedSubMeshCount) + { + subMeshIndices = new int[groups.Length][]; + for (var i = 0; i < groups.Length; i++) + { + var group = groups[i]; + if (group.firstIndex < 0 || group.indexCount < 0 || group.firstIndex + group.indexCount > triangles.Length) + { + reason = "Invalid submesh group range after decimation."; + return false; + } + + var slice = new int[group.indexCount]; + if (group.indexCount > 0) + { + Array.Copy(triangles, group.firstIndex, slice, 0, group.indexCount); + } + subMeshIndices[i] = slice; + } + } + else if (expectedSubMeshCount == 1) + { + subMeshIndices = [triangles]; + } + else + { + reason = "Submesh group count mismatch after decimation."; + return false; + } + + var vertexCount = decimatedShared.positions.Length; + var positions = decimatedShared.positions; + + var attrList = decimatedShared.attributes as Nano.MetaAttributeList; + if (attrList == null) + { + reason = "Missing vertex attributes after decimation."; + return false; + } + + Vector3[]? normals = format.HasNormals ? new Vector3[vertexCount] : null; + Vector4[]? tangents = format.HasTangent1 ? new Vector4[vertexCount] : null; + Vector4[]? tangents2 = format.HasTangent2 ? new Vector4[vertexCount] : null; + Vector4[]? colors = format.HasColors ? new Vector4[vertexCount] : null; + BoneWeight[]? boneWeights = format.HasSkinning ? new BoneWeight[vertexCount] : null; + float[]? positionWs = format.HasPositionW ? new float[vertexCount] : null; + float[]? normalWs = format.HasNormalW ? new float[vertexCount] : null; + + Vector2[][]? uvChannels = null; + if (format.UvChannelCount > 0) + { + uvChannels = new Vector2[format.UvChannelCount][]; + for (var channel = 0; channel < format.UvChannelCount; channel++) + { + uvChannels[channel] = new Vector2[vertexCount]; + } + } + + for (var i = 0; i < vertexCount; i++) + { + var attr = (Nano.MetaAttribute)attrList[i]; + var data = attr.attr0; + + if (normals != null) + { + normals[i] = data.normal; + } + + if (tangents != null) + { + tangents[i] = data.tangent1; + } + + if (tangents2 != null) + { + tangents2[i] = data.tangent2; + } + + if (colors != null) + { + colors[i] = data.color; + } + + if (boneWeights != null) + { + boneWeights[i] = data.boneWeight; + } + + if (positionWs != null) + { + positionWs[i] = data.positionW; + } + + if (normalWs != null) + { + normalWs[i] = data.normalW; + } + + if (uvChannels != null) + { + if (uvChannels.Length > 0) + { + uvChannels[0][i] = data.uv0; + } + if (uvChannels.Length > 1) + { + uvChannels[1][i] = data.uv1; + } + if (uvChannels.Length > 2) + { + uvChannels[2][i] = data.uv2; + } + if (uvChannels.Length > 3) + { + uvChannels[3][i] = data.uv3; + } + } + } + + decimated = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding); + decimatedSubMeshIndices = subMeshIndices; + return true; + } + + private static Nano.FfxivAttributeFlags BuildFfxivAttributeFlags(VertexFormat format) + { + var flags = Nano.FfxivAttributeFlags.None; + if (format.HasNormals) + { + flags |= Nano.FfxivAttributeFlags.Normal; + } + if (format.HasTangent1) + { + flags |= Nano.FfxivAttributeFlags.Tangent1; + } + if (format.HasTangent2) + { + flags |= Nano.FfxivAttributeFlags.Tangent2; + } + if (format.HasColors) + { + flags |= Nano.FfxivAttributeFlags.Color; + } + if (format.HasSkinning) + { + flags |= Nano.FfxivAttributeFlags.BoneWeights; + } + if (format.HasPositionW) + { + flags |= Nano.FfxivAttributeFlags.PositionW; + } + if (format.HasNormalW) + { + flags |= Nano.FfxivAttributeFlags.NormalW; + } + if (format.UvChannelCount > 0) + { + flags |= Nano.FfxivAttributeFlags.Uv0; + } + if (format.UvChannelCount > 1) + { + flags |= Nano.FfxivAttributeFlags.Uv1; + } + if (format.UvChannelCount > 2) + { + flags |= Nano.FfxivAttributeFlags.Uv2; + } + if (format.UvChannelCount > 3) + { + flags |= Nano.FfxivAttributeFlags.Uv3; + } + return flags; } private static bool TryDecodeMeshData( @@ -494,6 +2027,8 @@ internal static class MdlDecimator } } + var blendWeightEncoding = DetectBlendWeightEncoding(mdl, lodIndex, mesh, format); + var streams = new BinaryReader[MaxStreams]; for (var streamIndex = 0; streamIndex < MaxStreams; streamIndex++) { @@ -548,7 +2083,7 @@ internal static class MdlDecimator indices = ReadIndices(type, stream); break; case MdlFile.VertexUsage.BlendWeights: - weights = ReadWeights(type, stream); + weights = ReadWeights(type, stream, blendWeightEncoding); break; case MdlFile.VertexUsage.UV when uvChannels != null: if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement)) @@ -577,17 +2112,17 @@ internal static class MdlDecimator return false; } - NormalizeWeights(weights); boneWeights[vertexIndex] = new BoneWeight(indices[0], indices[1], indices[2], indices[3], weights[0], weights[1], weights[2], weights[3]); } } - decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs); + decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding); return true; } private static bool TryEncodeMeshData( - Mesh decimatedMesh, + DecodedMeshData decimated, + int[][] decimatedSubMeshIndices, VertexFormat format, MeshStruct originalMesh, MdlStructs.SubmeshStruct[] originalSubMeshes, @@ -604,20 +2139,26 @@ internal static class MdlDecimator indices = []; reason = null; - var vertexCount = decimatedMesh.Vertices.Length; + if (decimatedSubMeshIndices.Length != originalSubMeshes.Length) + { + reason = "Decimated submesh count mismatch."; + return false; + } + + var vertexCount = decimated.Positions.Length; if (vertexCount > ushort.MaxValue) { reason = "Vertex count exceeds ushort range."; return false; } - var normals = decimatedMesh.Normals; - var tangents = decimatedMesh.Tangents; - var tangents2 = decimatedMesh.Tangents2; - var colors = decimatedMesh.Colors; - var boneWeights = decimatedMesh.BoneWeights; - var positionWs = decimatedMesh.PositionWs; - var normalWs = decimatedMesh.NormalWs; + var normals = decimated.Normals; + var tangents = decimated.Tangents; + var tangents2 = decimated.Tangents2; + var colors = decimated.Colors; + var boneWeights = decimated.BoneWeights; + var positionWs = decimated.PositionWs; + var normalWs = decimated.NormalWs; if (format.HasNormals && normals == null) { @@ -670,16 +2211,13 @@ internal static class MdlDecimator var uvChannels = Array.Empty(); if (format.UvChannelCount > 0) { - uvChannels = new Vector2[format.UvChannelCount][]; - for (var channel = 0; channel < format.UvChannelCount; channel++) + if (decimated.UvChannels == null || decimated.UvChannels.Length < format.UvChannelCount) { - if (decimatedMesh.GetUVDimension(channel) != 2) - { - reason = "Unsupported UV dimension after decimation."; - return false; - } - uvChannels[channel] = decimatedMesh.GetUVs2D(channel); + reason = "Missing UV channels after decimation."; + return false; } + + uvChannels = decimated.UvChannels; } var streamBuffers = new byte[MaxStreams][]; @@ -732,7 +2270,7 @@ internal static class MdlDecimator switch (usage) { case MdlFile.VertexUsage.Position: - WritePosition(type, decimatedMesh.Vertices[vertexIndex], target, positionWs != null ? positionWs[vertexIndex] : null); + WritePosition(type, decimated.Positions[vertexIndex], target, positionWs != null ? positionWs[vertexIndex] : null); break; case MdlFile.VertexUsage.Normal when normals != null: WriteNormal(type, normals[vertexIndex], target, normalWs != null ? normalWs[vertexIndex] : null); @@ -750,7 +2288,7 @@ internal static class MdlDecimator WriteBlendIndices(type, boneWeights[vertexIndex], target); break; case MdlFile.VertexUsage.BlendWeights when boneWeights != null: - WriteBlendWeights(type, boneWeights[vertexIndex], target); + WriteBlendWeights(type, boneWeights[vertexIndex], decimated.BlendWeightEncoding, target); break; case MdlFile.VertexUsage.UV when format.UvChannelCount > 0: if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement)) @@ -771,7 +2309,7 @@ internal static class MdlDecimator for (var subMeshIndex = 0; subMeshIndex < originalSubMeshes.Length; subMeshIndex++) { - var subMeshIndices = decimatedMesh.GetIndices(subMeshIndex); + var subMeshIndices = decimatedSubMeshIndices[subMeshIndex]; if (subMeshIndices.Any(index => index < 0 || index >= vertexCount)) { reason = "Decimated indices out of range."; @@ -1106,7 +2644,7 @@ internal static class MdlDecimator || type == MdlFile.VertexType.UShort2 || type == MdlFile.VertexType.Single1) { - if (uvChannelCount + 1 > Mesh.UVChannelCount) + if (uvChannelCount + 1 > MaxUvChannels) { reason = "Too many UV channels."; return false; @@ -1121,7 +2659,7 @@ internal static class MdlDecimator || type == MdlFile.VertexType.NShort4 || type == MdlFile.VertexType.UShort4) { - if (uvChannelCount + 2 > Mesh.UVChannelCount) + if (uvChannelCount + 2 > MaxUvChannels) { reason = "Too many UV channels."; return false; @@ -1237,20 +2775,24 @@ internal static class MdlDecimator for (var i = 0; i < tangents.Length; i++) { var tangent = tangents[i]; - var length = MathF.Sqrt(tangent.x * tangent.x + tangent.y * tangent.y + tangent.z * tangent.z); + var x = tangent.x; + var y = tangent.y; + var z = tangent.z; + var w = tangent.w; + var length = MathF.Sqrt((x * x) + (y * y) + (z * z)); if (length > 1e-6f) { - tangent.x /= length; - tangent.y /= length; - tangent.z /= length; + x /= length; + y /= length; + z /= length; } if (clampW) { - tangent.w = tangent.w >= 0f ? 1f : -1f; + w = w >= 0f ? 1f : -1f; } - tangents[i] = tangent; + tangents[i] = new Vector4(x, y, z, w); } } @@ -1271,7 +2813,7 @@ internal static class MdlDecimator MdlFile.VertexType.NShort2 => ReadUShort2Normalized(reader), MdlFile.VertexType.UShort2 => ReadUShort2Normalized(reader), MdlFile.VertexType.Single1 => new Vector2(reader.ReadSingle(), 0f), - _ => Vector2.zero, + _ => Vector2.Zero, }; uvChannels[mapping.FirstChannel][vertexIndex] = uv; @@ -1291,7 +2833,7 @@ internal static class MdlDecimator MdlFile.VertexType.Short4 => ReadShort4(reader), MdlFile.VertexType.NShort4 => ReadUShort4Normalized(reader), MdlFile.VertexType.UShort4 => ReadUShort4Normalized(reader), - _ => Vector4.zero, + _ => new Vector4(0f, 0f, 0f, 0f), }; uvChannels[mapping.FirstChannel][vertexIndex] = new Vector2(uv.x, uv.y); @@ -1312,7 +2854,7 @@ internal static class MdlDecimator }; } - private static float[] ReadWeights(MdlFile.VertexType type, BinaryReader reader) + private static float[] ReadWeights(MdlFile.VertexType type, BinaryReader reader, BlendWeightEncoding encoding) { return type switch { @@ -1320,11 +2862,22 @@ internal static class MdlDecimator MdlFile.VertexType.NByte4 => ReadUByte4(reader).ToFloatArray(), MdlFile.VertexType.Single4 => new[] { reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle() }, MdlFile.VertexType.NShort4 => ReadUShort4Normalized(reader).ToFloatArray(), - MdlFile.VertexType.UShort4 => ReadUShort4Normalized(reader).ToFloatArray(), + MdlFile.VertexType.UShort4 => encoding == BlendWeightEncoding.UShortAsByte + ? ReadUShort4AsByte(reader) + : ReadUShort4Normalized(reader).ToFloatArray(), _ => throw new InvalidOperationException($"Unsupported weights type {type}"), }; } + private static float[] ReadUShort4AsByte(BinaryReader reader) + { + var w0 = reader.ReadUInt16(); + var w1 = reader.ReadUInt16(); + var w2 = reader.ReadUInt16(); + var w3 = reader.ReadUInt16(); + return new[] { w0 / 255f, w1 / 255f, w2 / 255f, w3 / 255f }; + } + private static Vector4 ReadUByte4(BinaryReader reader) { return new Vector4( @@ -1408,7 +2961,7 @@ internal static class MdlDecimator case MdlFile.VertexType.UShort4: return ReadUShort4Normalized(reader); default: - return Vector4.zero; + return new Vector4(0f, 0f, 0f, 0f); } } @@ -1469,30 +3022,30 @@ internal static class MdlDecimator { if (type == MdlFile.VertexType.UByte4) { - target[0] = (byte)Math.Clamp(weights.boneIndex0, 0, 255); - target[1] = (byte)Math.Clamp(weights.boneIndex1, 0, 255); - target[2] = (byte)Math.Clamp(weights.boneIndex2, 0, 255); - target[3] = (byte)Math.Clamp(weights.boneIndex3, 0, 255); + target[0] = (byte)Math.Clamp(weights.index0, 0, 255); + target[1] = (byte)Math.Clamp(weights.index1, 0, 255); + target[2] = (byte)Math.Clamp(weights.index2, 0, 255); + target[3] = (byte)Math.Clamp(weights.index3, 0, 255); return; } if (type == MdlFile.VertexType.UShort4) { - BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShort(weights.boneIndex0)); - BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShort(weights.boneIndex1)); - BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), ToUShort(weights.boneIndex2)); - BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), ToUShort(weights.boneIndex3)); + BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShort(weights.index0)); + BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShort(weights.index1)); + BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), ToUShort(weights.index2)); + BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), ToUShort(weights.index3)); } } - private static void WriteBlendWeights(MdlFile.VertexType type, BoneWeight weights, Span target) + private static void WriteBlendWeights(MdlFile.VertexType type, BoneWeight weights, BlendWeightEncoding encoding, Span target) { if (type == MdlFile.VertexType.Single4) { - BinaryPrimitives.WriteSingleLittleEndian(target[..4], weights.boneWeight0); - BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), weights.boneWeight1); - BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), weights.boneWeight2); - BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), weights.boneWeight3); + BinaryPrimitives.WriteSingleLittleEndian(target[..4], weights.weight0); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), weights.weight1); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), weights.weight2); + BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), weights.weight3); return; } @@ -1504,11 +3057,16 @@ internal static class MdlDecimator return; } - var w0 = Clamp01(weights.boneWeight0); - var w1 = Clamp01(weights.boneWeight1); - var w2 = Clamp01(weights.boneWeight2); - var w3 = Clamp01(weights.boneWeight3); - NormalizeWeights(ref w0, ref w1, ref w2, ref w3); + var w0 = Clamp01(weights.weight0); + var w1 = Clamp01(weights.weight1); + var w2 = Clamp01(weights.weight2); + var w3 = Clamp01(weights.weight3); + + if (type == MdlFile.VertexType.UShort4 && encoding == BlendWeightEncoding.UShortAsByte) + { + WriteUShort4AsByte(w0, w1, w2, w3, target); + return; + } if (type == MdlFile.VertexType.UShort4 || type == MdlFile.VertexType.NShort4) { @@ -1519,10 +3077,16 @@ internal static class MdlDecimator return; } - target[0] = ToByte(w0); - target[1] = ToByte(w1); - target[2] = ToByte(w2); - target[3] = ToByte(w3); + WriteByteWeights(w0, w1, w2, w3, target); + } + + private static void WriteUShort4AsByte(float w0, float w1, float w2, float w3, Span target) + { + QuantizeByteWeights(w0, w1, w2, w3, out var b0, out var b1, out var b2, out var b3); + BinaryPrimitives.WriteUInt16LittleEndian(target[..2], (ushort)b0); + BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), (ushort)b1); + BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), (ushort)b2); + BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), (ushort)b3); } private static void WriteUv(MdlFile.VertexType type, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex, Span target) @@ -1548,7 +3112,7 @@ internal static class MdlDecimator var uv0 = uvChannels[mapping.FirstChannel][vertexIndex]; var uv1 = mapping.SecondChannel.HasValue ? uvChannels[mapping.SecondChannel.Value][vertexIndex] - : Vector2.zero; + : Vector2.Zero; WriteVector4(type, new Vector4(uv0.x, uv0.y, uv1.x, uv1.y), target); } } @@ -1672,7 +3236,7 @@ internal static class MdlDecimator private static void WriteNByte4(Vector4 value, Span target) { - var normalized = (value * 0.5f) + new Vector4(0.5f); + var normalized = (value * 0.5f) + new Vector4(0.5f, 0.5f, 0.5f, 0.5f); WriteUByte4(normalized, target); } @@ -1746,6 +3310,101 @@ internal static class MdlDecimator private static byte ToByte(float value) => (byte)Math.Clamp((int)Math.Round(value * 255f), 0, 255); + private static void WriteByteWeights(float w0, float w1, float w2, float w3, Span target) + { + QuantizeByteWeights(w0, w1, w2, w3, out var b0, out var b1, out var b2, out var b3); + target[0] = (byte)b0; + target[1] = (byte)b1; + target[2] = (byte)b2; + target[3] = (byte)b3; + } + + private static void QuantizeByteWeights(float w0, float w1, float w2, float w3, out int b0, out int b1, out int b2, out int b3) + { + var sum = w0 + w1 + w2 + w3; + if (sum <= 1e-6f) + { + w0 = 1f; + w1 = 0f; + w2 = 0f; + w3 = 0f; + sum = 1f; + } + + var targetSum = (int)MathF.Round(sum * 255f); + if (sum > 0f && targetSum == 0) + { + targetSum = 1; + } + + targetSum = Math.Clamp(targetSum, 0, 255); + if (targetSum == 0) + { + b0 = 0; + b1 = 0; + b2 = 0; + b3 = 0; + return; + } + + var scale = targetSum / sum; + var scaled0 = w0 * scale; + var scaled1 = w1 * scale; + var scaled2 = w2 * scale; + var scaled3 = w3 * scale; + + b0 = (int)MathF.Floor(scaled0); + b1 = (int)MathF.Floor(scaled1); + b2 = (int)MathF.Floor(scaled2); + b3 = (int)MathF.Floor(scaled3); + + var remainder = targetSum - (b0 + b1 + b2 + b3); + if (remainder > 0) + { + Span fractions = stackalloc float[4]; + fractions[0] = scaled0 - b0; + fractions[1] = scaled1 - b1; + fractions[2] = scaled2 - b2; + fractions[3] = scaled3 - b3; + + Span order = stackalloc int[4] { 0, 1, 2, 3 }; + for (var i = 0; i < order.Length - 1; i++) + { + for (var j = i + 1; j < order.Length; j++) + { + if (fractions[order[j]] > fractions[order[i]]) + { + (order[i], order[j]) = (order[j], order[i]); + } + } + } + + for (var i = 0; i < remainder && i < order.Length; i++) + { + switch (order[i]) + { + case 0: + b0++; + break; + case 1: + b1++; + break; + case 2: + b2++; + break; + case 3: + b3++; + break; + } + } + } + + b0 = Math.Clamp(b0, 0, 255); + b1 = Math.Clamp(b1, 0, 255); + b2 = Math.Clamp(b2, 0, 255); + b3 = Math.Clamp(b3, 0, 255); + } + private static short ToShort(float value) => (short)Math.Clamp((int)Math.Round(value), short.MinValue, short.MaxValue); @@ -1794,6 +3453,50 @@ internal static class MdlDecimator w3 /= sum; } + private static BlendWeightEncoding DetectBlendWeightEncoding(MdlFile mdl, int lodIndex, MeshStruct mesh, VertexFormat format) + { + if (!format.BlendWeightsElement.HasValue) + { + return BlendWeightEncoding.Default; + } + + var blendWeightsElement = format.BlendWeightsElement.Value; + if ((MdlFile.VertexType)blendWeightsElement.Type != MdlFile.VertexType.UShort4) + { + return BlendWeightEncoding.Default; + } + + var stride = mesh.VertexBufferStride(blendWeightsElement.Stream); + if (stride == 0 || mesh.VertexCount == 0) + { + return BlendWeightEncoding.Default; + } + + var elementSize = GetElementSize(MdlFile.VertexType.UShort4); + var baseOffset = (int)(mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(blendWeightsElement.Stream)); + var data = mdl.RemainingData.AsSpan(); + + for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++) + { + var offset = baseOffset + (vertexIndex * stride) + blendWeightsElement.Offset; + if (offset < 0 || offset + elementSize > data.Length) + { + return BlendWeightEncoding.Default; + } + + var w0 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset, 2)); + var w1 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset + 2, 2)); + var w2 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset + 4, 2)); + var w3 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset + 6, 2)); + if (w0 > byte.MaxValue || w1 > byte.MaxValue || w2 > byte.MaxValue || w3 > byte.MaxValue) + { + return BlendWeightEncoding.Default; + } + } + + return BlendWeightEncoding.UShortAsByte; + } + private static int GetElementSize(MdlFile.VertexType type) => type switch { @@ -1814,6 +3517,12 @@ internal static class MdlDecimator _ => throw new InvalidOperationException($"Unsupported vertex type {type}"), }; + private enum BlendWeightEncoding + { + Default, + UShortAsByte, + } + private readonly record struct ElementKey(byte Stream, byte Offset, byte Type, byte Usage, byte UsageIndex) { public static ElementKey From(MdlStructs.VertexElement element) @@ -1879,7 +3588,8 @@ internal static class MdlDecimator BoneWeight[]? boneWeights, Vector2[][]? uvChannels, float[]? positionWs, - float[]? normalWs) + float[]? normalWs, + BlendWeightEncoding blendWeightEncoding) { Positions = positions; Normals = normals; @@ -1890,6 +3600,7 @@ internal static class MdlDecimator UvChannels = uvChannels; PositionWs = positionWs; NormalWs = normalWs; + BlendWeightEncoding = blendWeightEncoding; } public Vector3d[] Positions { get; } @@ -1901,10 +3612,11 @@ internal static class MdlDecimator public Vector2[][]? UvChannels { get; } public float[]? PositionWs { get; } public float[]? NormalWs { get; } + public BlendWeightEncoding BlendWeightEncoding { get; } } } -internal static class MeshDecimatorVectorExtensions +internal static class NanomeshVectorExtensions { public static Vector3 ToVector3(this Vector4 value) => new(value.x, value.y, value.z); diff --git a/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs b/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs index 98f1f88..0195a0c 100644 --- a/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs +++ b/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs @@ -1,5 +1,6 @@ using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; +using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; @@ -71,11 +72,50 @@ public sealed class ModelDecimationService }); } + public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings) + { + if (!ShouldScheduleBatchDecimation(hash, filePath, settings)) + { + return; + } + + if (_decimationDeduplicator.TryGetExisting(hash, out _)) + { + return; + } + + _failedHashes.TryRemove(hash, out _); + _decimatedPaths.TryRemove(hash, out _); + + _logger.LogInformation("Queued batch model decimation for {Hash}", hash); + + _decimationDeduplicator.GetOrStart(hash, async () => + { + await _decimationSemaphore.WaitAsync().ConfigureAwait(false); + try + { + await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false); + } + catch (Exception ex) + { + _failedHashes[hash] = 1; + _logger.LogWarning(ex, "Batch model decimation failed for {Hash}", hash); + } + finally + { + _decimationSemaphore.Release(); + } + }); + } + public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null) - => IsDecimationEnabled() + { + var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold); + return IsDecimationEnabled() && filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase) && IsDecimationAllowed(gamePath) - && !ShouldSkipByTriangleCache(hash); + && !ShouldSkipByTriangleCache(hash, threshold); + } public string GetPreferredPath(string hash, string originalPath) { @@ -131,6 +171,23 @@ public sealed class ModelDecimationService } private Task DecimateInternalAsync(string hash, string sourcePath) + { + if (!TryGetDecimationSettings(out var settings)) + { + _logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash); + return Task.CompletedTask; + } + + return DecimateInternalAsync(hash, sourcePath, settings, allowExisting: true); + } + + private Task DecimateInternalAsync( + string hash, + string sourcePath, + ModelDecimationSettings settings, + bool allowExisting, + string? destinationOverride = null, + bool registerDecimatedPath = true) { if (!File.Exists(sourcePath)) { @@ -139,34 +196,47 @@ public sealed class ModelDecimationService return Task.CompletedTask; } - if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio, out var normalizeTangents)) + if (!TryNormalizeSettings(settings, out var normalized)) { - _logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash); + _logger.LogInformation("Model decimation skipped for {Hash}; invalid settings.", hash); return Task.CompletedTask; } _logger.LogInformation( - "Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents})", + "Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents}, avoid body intersection {AvoidBodyIntersection})", hash, - triangleThreshold, - targetRatio, - normalizeTangents); + normalized.TriangleThreshold, + normalized.TargetRatio, + normalized.NormalizeTangents, + normalized.AvoidBodyIntersection); - var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl"); - if (File.Exists(destination)) + var destination = destinationOverride ?? Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl"); + var inPlace = string.Equals(destination, sourcePath, StringComparison.OrdinalIgnoreCase); + if (!inPlace && File.Exists(destination)) { - RegisterDecimatedModel(hash, sourcePath, destination); - return Task.CompletedTask; + if (allowExisting) + { + if (registerDecimatedPath) + { + RegisterDecimatedModel(hash, sourcePath, destination); + } + return Task.CompletedTask; + } + + TryDelete(destination); } - if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, normalizeTangents, _logger)) + if (!MdlDecimator.TryDecimate(sourcePath, destination, normalized, _logger)) { _failedHashes[hash] = 1; _logger.LogInformation("Model decimation skipped for {Hash}", hash); return Task.CompletedTask; } - RegisterDecimatedModel(hash, sourcePath, destination); + if (registerDecimatedPath) + { + RegisterDecimatedModel(hash, sourcePath, destination); + } _logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination); return Task.CompletedTask; } @@ -255,7 +325,7 @@ public sealed class ModelDecimationService private bool IsDecimationEnabled() => _performanceConfigService.Current.EnableModelDecimation; - private bool ShouldSkipByTriangleCache(string hash) + private bool ShouldSkipByTriangleCache(string hash, int triangleThreshold) { if (string.IsNullOrEmpty(hash)) { @@ -267,7 +337,7 @@ public sealed class ModelDecimationService return false; } - var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold); + var threshold = Math.Max(0, triangleThreshold); return threshold > 0 && cachedTris < threshold; } @@ -318,11 +388,14 @@ public sealed class ModelDecimationService private static string NormalizeGamePath(string path) => path.Replace('\\', '/').ToLowerInvariant(); - private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio, out bool normalizeTangents) + private bool TryGetDecimationSettings(out ModelDecimationSettings settings) { - triangleThreshold = 15_000; - targetRatio = 0.8; - normalizeTangents = true; + settings = new ModelDecimationSettings( + ModelDecimationDefaults.TriangleThreshold, + ModelDecimationDefaults.TargetRatio, + ModelDecimationDefaults.NormalizeTangents, + ModelDecimationDefaults.AvoidBodyIntersection, + new ModelDecimationAdvancedSettings()); var config = _performanceConfigService.Current; if (!config.EnableModelDecimation) @@ -330,15 +403,86 @@ public sealed class ModelDecimationService return false; } - triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold); - targetRatio = config.ModelDecimationTargetRatio; - normalizeTangents = config.ModelDecimationNormalizeTangents; - if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio)) + var advanced = NormalizeAdvancedSettings(config.ModelDecimationAdvanced); + settings = new ModelDecimationSettings( + Math.Max(0, config.ModelDecimationTriangleThreshold), + config.ModelDecimationTargetRatio, + config.ModelDecimationNormalizeTangents, + config.ModelDecimationAvoidBodyIntersection, + advanced); + + return TryNormalizeSettings(settings, out settings); + } + + private static bool TryNormalizeSettings(ModelDecimationSettings settings, out ModelDecimationSettings normalized) + { + var ratio = settings.TargetRatio; + if (double.IsNaN(ratio) || double.IsInfinity(ratio)) + { + normalized = default; + return false; + } + + ratio = Math.Clamp(ratio, MinTargetRatio, MaxTargetRatio); + var advanced = NormalizeAdvancedSettings(settings.Advanced); + normalized = new ModelDecimationSettings( + Math.Max(0, settings.TriangleThreshold), + ratio, + settings.NormalizeTangents, + settings.AvoidBodyIntersection, + advanced); + return true; + } + + private static ModelDecimationAdvancedSettings NormalizeAdvancedSettings(ModelDecimationAdvancedSettings? settings) + { + var source = settings ?? new ModelDecimationAdvancedSettings(); + return new ModelDecimationAdvancedSettings + { + MinComponentTriangles = Math.Clamp(source.MinComponentTriangles, 0, 1000), + MaxCollapseEdgeLengthFactor = ClampFloat(source.MaxCollapseEdgeLengthFactor, 0.1f, 10f, ModelDecimationAdvancedSettings.DefaultMaxCollapseEdgeLengthFactor), + NormalSimilarityThresholdDegrees = ClampFloat(source.NormalSimilarityThresholdDegrees, 0f, 180f, ModelDecimationAdvancedSettings.DefaultNormalSimilarityThresholdDegrees), + BoneWeightSimilarityThreshold = ClampFloat(source.BoneWeightSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBoneWeightSimilarityThreshold), + UvSimilarityThreshold = ClampFloat(source.UvSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultUvSimilarityThreshold), + UvSeamAngleCos = ClampFloat(source.UvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultUvSeamAngleCos), + BlockUvSeamVertices = source.BlockUvSeamVertices, + AllowBoundaryCollapses = source.AllowBoundaryCollapses, + BodyCollisionDistanceFactor = ClampFloat(source.BodyCollisionDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionDistanceFactor), + BodyCollisionNoOpDistanceFactor = ClampFloat(source.BodyCollisionNoOpDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpDistanceFactor), + BodyCollisionAdaptiveRelaxFactor = ClampFloat(source.BodyCollisionAdaptiveRelaxFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveRelaxFactor), + BodyCollisionAdaptiveNearRatio = ClampFloat(source.BodyCollisionAdaptiveNearRatio, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveNearRatio), + BodyCollisionAdaptiveUvThreshold = ClampFloat(source.BodyCollisionAdaptiveUvThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveUvThreshold), + BodyCollisionNoOpUvSeamAngleCos = ClampFloat(source.BodyCollisionNoOpUvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpUvSeamAngleCos), + BodyCollisionProtectionFactor = ClampFloat(source.BodyCollisionProtectionFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProtectionFactor), + BodyProxyTargetRatioMin = ClampFloat(source.BodyProxyTargetRatioMin, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyProxyTargetRatioMin), + BodyCollisionProxyInflate = ClampFloat(source.BodyCollisionProxyInflate, 0f, 0.1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProxyInflate), + BodyCollisionPenetrationFactor = ClampFloat(source.BodyCollisionPenetrationFactor, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionPenetrationFactor), + MinBodyCollisionDistance = ClampFloat(source.MinBodyCollisionDistance, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionDistance), + MinBodyCollisionCellSize = ClampFloat(source.MinBodyCollisionCellSize, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionCellSize), + }; + } + + private static float ClampFloat(float value, float min, float max, float fallback) + { + if (float.IsNaN(value) || float.IsInfinity(value)) + { + return fallback; + } + + return Math.Clamp(value, min, max); + } + + private bool ShouldScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings) + { + if (string.IsNullOrWhiteSpace(filePath) || !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) { return false; } - targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio); + if (!TryNormalizeSettings(settings, out _)) + { + return false; + } return true; } diff --git a/LightlessSync/Services/ModelDecimation/ModelDecimationSettings.cs b/LightlessSync/Services/ModelDecimation/ModelDecimationSettings.cs new file mode 100644 index 0000000..4b5adc2 --- /dev/null +++ b/LightlessSync/Services/ModelDecimation/ModelDecimationSettings.cs @@ -0,0 +1,10 @@ +using LightlessSync.LightlessConfiguration.Configurations; + +namespace LightlessSync.Services.ModelDecimation; + +public readonly record struct ModelDecimationSettings( + int TriangleThreshold, + double TargetRatio, + bool NormalizeTangents, + bool AvoidBodyIntersection, + ModelDecimationAdvancedSettings Advanced); diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index 65d9346..f4b2a22 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -666,6 +666,20 @@ public sealed partial class XivDataAnalyzer return CalculateTrianglesFromPath(hash, path.ResolvedFilepath, _configService.Current.TriangleDictionary, _failedCalculatedTris); } + public long RefreshTrianglesForPath(string hash, string filePath) + { + if (string.IsNullOrEmpty(filePath) + || !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase) + || !File.Exists(filePath)) + { + return 0; + } + + _failedCalculatedTris.RemoveAll(entry => entry.Equals(hash, StringComparison.Ordinal)); + _configService.Current.TriangleDictionary.TryRemove(hash, out _); + return CalculateTrianglesFromPath(hash, filePath, _configService.Current.TriangleDictionary, _failedCalculatedTris); + } + public async Task GetEffectiveTrianglesByHash(string hash, string filePath) { if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0) diff --git a/LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs b/LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs deleted file mode 100644 index 723eef6..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs +++ /dev/null @@ -1,169 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using Microsoft.Extensions.Logging; - -namespace MeshDecimator.Algorithms -{ - /// - /// A decimation algorithm. - /// - public abstract class DecimationAlgorithm - { - #region Delegates - /// - /// A callback for decimation status reports. - /// - /// The current iteration, starting at zero. - /// The original count of triangles. - /// The current count of triangles. - /// The target count of triangles. - public delegate void StatusReportCallback(int iteration, int originalTris, int currentTris, int targetTris); - #endregion - - #region Fields - private bool preserveBorders = false; - private int maxVertexCount = 0; - private bool verbose = false; - - private StatusReportCallback statusReportInvoker = null; - #endregion - - #region Properties - /// - /// Gets or sets if borders should be kept. - /// Default value: false - /// - [Obsolete("Use the 'DecimationAlgorithm.PreserveBorders' property instead.", false)] - public bool KeepBorders - { - get { return preserveBorders; } - set { preserveBorders = value; } - } - - /// - /// Gets or sets if borders should be preserved. - /// Default value: false - /// - public bool PreserveBorders - { - get { return preserveBorders; } - set { preserveBorders = value; } - } - - /// - /// Gets or sets if linked vertices should be kept. - /// Default value: false - /// - [Obsolete("This feature has been removed, for more details why please read the readme.", true)] - public bool KeepLinkedVertices - { - get { return false; } - set { } - } - - /// - /// Gets or sets the maximum vertex count. Set to zero for no limitation. - /// Default value: 0 (no limitation) - /// - public int MaxVertexCount - { - get { return maxVertexCount; } - set { maxVertexCount = Math.MathHelper.Max(value, 0); } - } - - /// - /// Gets or sets if verbose information should be printed in the console. - /// Default value: false - /// - public bool Verbose - { - get { return verbose; } - set { verbose = value; } - } - - /// - /// Gets or sets the logger used for diagnostics. - /// - public ILogger? Logger { get; set; } - #endregion - - #region Events - /// - /// An event for status reports for this algorithm. - /// - public event StatusReportCallback StatusReport - { - add { statusReportInvoker += value; } - remove { statusReportInvoker -= value; } - } - #endregion - - #region Protected Methods - /// - /// Reports the current status of the decimation. - /// - /// The current iteration, starting at zero. - /// The original count of triangles. - /// The current count of triangles. - /// The target count of triangles. - protected void ReportStatus(int iteration, int originalTris, int currentTris, int targetTris) - { - var statusReportInvoker = this.statusReportInvoker; - if (statusReportInvoker != null) - { - statusReportInvoker.Invoke(iteration, originalTris, currentTris, targetTris); - } - } - #endregion - - #region Public Methods - /// - /// Initializes the algorithm with the original mesh. - /// - /// The mesh. - public abstract void Initialize(Mesh mesh); - - /// - /// Decimates the mesh. - /// - /// The target triangle count. - public abstract void DecimateMesh(int targetTrisCount); - - /// - /// Decimates the mesh without losing any quality. - /// - public abstract void DecimateMeshLossless(); - - /// - /// Returns the resulting mesh. - /// - /// The resulting mesh. - public abstract Mesh ToMesh(); - #endregion - } -} diff --git a/LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs b/LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs deleted file mode 100644 index 31c001d..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs +++ /dev/null @@ -1,1627 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -#region Original License -///////////////////////////////////////////// -// -// Mesh Simplification Tutorial -// -// (C) by Sven Forstmann in 2014 -// -// License : MIT -// http://opensource.org/licenses/MIT -// -//https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification -#endregion - -using System; -using System.Collections.Generic; -using MeshDecimator.Collections; -using MeshDecimator.Math; -using Microsoft.Extensions.Logging; - -namespace MeshDecimator.Algorithms -{ - /// - /// The fast quadric mesh simplification algorithm. - /// - public sealed class FastQuadricMeshSimplification : DecimationAlgorithm - { - #region Consts - private const double DoubleEpsilon = 1.0E-3; - #endregion - - #region Classes - #region Triangle - private struct Triangle - { - #region Fields - public int v0; - public int v1; - public int v2; - public int subMeshIndex; - - public int va0; - public int va1; - public int va2; - - public double err0; - public double err1; - public double err2; - public double err3; - - public bool deleted; - public bool dirty; - public Vector3d n; - #endregion - - #region Properties - public int this[int index] - { - get - { - return (index == 0 ? v0 : (index == 1 ? v1 : v2)); - } - set - { - switch (index) - { - case 0: - v0 = value; - break; - case 1: - v1 = value; - break; - case 2: - v2 = value; - break; - default: - throw new IndexOutOfRangeException(); - } - } - } - #endregion - - #region Constructor - public Triangle(int v0, int v1, int v2, int subMeshIndex) - { - this.v0 = v0; - this.v1 = v1; - this.v2 = v2; - this.subMeshIndex = subMeshIndex; - - this.va0 = v0; - this.va1 = v1; - this.va2 = v2; - - err0 = err1 = err2 = err3 = 0; - deleted = dirty = false; - n = new Vector3d(); - } - #endregion - - #region Public Methods - public void GetAttributeIndices(int[] attributeIndices) - { - attributeIndices[0] = va0; - attributeIndices[1] = va1; - attributeIndices[2] = va2; - } - - public void SetAttributeIndex(int index, int value) - { - switch (index) - { - case 0: - va0 = value; - break; - case 1: - va1 = value; - break; - case 2: - va2 = value; - break; - default: - throw new IndexOutOfRangeException(); - } - } - - public void GetErrors(double[] err) - { - err[0] = err0; - err[1] = err1; - err[2] = err2; - } - #endregion - } - #endregion - - #region Vertex - private struct Vertex - { - public Vector3d p; - public int tstart; - public int tcount; - public SymmetricMatrix q; - public bool border; - public bool seam; - public bool foldover; - - public Vertex(Vector3d p) - { - this.p = p; - this.tstart = 0; - this.tcount = 0; - this.q = new SymmetricMatrix(); - this.border = true; - this.seam = false; - this.foldover = false; - } - } - #endregion - - #region Ref - private struct Ref - { - public int tid; - public int tvertex; - - public void Set(int tid, int tvertex) - { - this.tid = tid; - this.tvertex = tvertex; - } - } - #endregion - - #region Border Vertex - private struct BorderVertex - { - public int index; - public int hash; - - public BorderVertex(int index, int hash) - { - this.index = index; - this.hash = hash; - } - } - #endregion - - #region Border Vertex Comparer - private class BorderVertexComparer : IComparer - { - public static readonly BorderVertexComparer instance = new BorderVertexComparer(); - - public int Compare(BorderVertex x, BorderVertex y) - { - return x.hash.CompareTo(y.hash); - } - } - #endregion - #endregion - - #region Fields - private bool preserveSeams = false; - private bool preserveFoldovers = false; - private bool enableSmartLink = true; - private int maxIterationCount = 100; - private double agressiveness = 7.0; - private double vertexLinkDistanceSqr = double.Epsilon; - - private int subMeshCount = 0; - private ResizableArray triangles = null; - private ResizableArray vertices = null; - private ResizableArray refs = null; - - private ResizableArray vertNormals = null; - private ResizableArray vertTangents = null; - private ResizableArray vertTangents2 = null; - private UVChannels vertUV2D = null; - private UVChannels vertUV3D = null; - private UVChannels vertUV4D = null; - private ResizableArray vertColors = null; - private ResizableArray vertBoneWeights = null; - private ResizableArray vertPositionWs = null; - private ResizableArray vertNormalWs = null; - - private int remainingVertices = 0; - - // Pre-allocated buffers - private double[] errArr = new double[3]; - private int[] attributeIndexArr = new int[3]; - #endregion - - #region Properties - /// - /// Gets or sets if seams should be preserved. - /// Default value: false - /// - public bool PreserveSeams - { - get { return preserveSeams; } - set { preserveSeams = value; } - } - - /// - /// Gets or sets if foldovers should be preserved. - /// Default value: false - /// - public bool PreserveFoldovers - { - get { return preserveFoldovers; } - set { preserveFoldovers = value; } - } - - /// - /// Gets or sets if a feature for smarter vertex linking should be enabled, reducing artifacts in the - /// decimated result at the cost of a slightly more expensive initialization by treating vertices at - /// the same position as the same vertex while separating the attributes. - /// Default value: true - /// - public bool EnableSmartLink - { - get { return enableSmartLink; } - set { enableSmartLink = value; } - } - - /// - /// Gets or sets the maximum iteration count. Higher number is more expensive but can bring you closer to your target quality. - /// Sometimes a lower maximum count might be desired in order to lower the performance cost. - /// Default value: 100 - /// - public int MaxIterationCount - { - get { return maxIterationCount; } - set { maxIterationCount = value; } - } - - /// - /// Gets or sets the agressiveness of this algorithm. Higher number equals higher quality, but more expensive to run. - /// Default value: 7.0 - /// - public double Agressiveness - { - get { return agressiveness; } - set { agressiveness = value; } - } - - /// - /// Gets or sets the maximum squared distance between two vertices in order to link them. - /// Note that this value is only used if EnableSmartLink is true. - /// Default value: double.Epsilon - /// - public double VertexLinkDistanceSqr - { - get { return vertexLinkDistanceSqr; } - set { vertexLinkDistanceSqr = value; } - } - #endregion - - #region Constructor - /// - /// Creates a new fast quadric mesh simplification algorithm. - /// - public FastQuadricMeshSimplification() - { - triangles = new ResizableArray(0); - vertices = new ResizableArray(0); - refs = new ResizableArray(0); - } - #endregion - - #region Private Methods - #region Initialize Vertex Attribute - private ResizableArray InitializeVertexAttribute(T[] attributeValues, string attributeName) - { - if (attributeValues != null && attributeValues.Length == vertices.Length) - { - var newArray = new ResizableArray(attributeValues.Length, attributeValues.Length); - var newArrayData = newArray.Data; - Array.Copy(attributeValues, 0, newArrayData, 0, attributeValues.Length); - return newArray; - } - else if (attributeValues != null && attributeValues.Length > 0) - { - Logger?.LogError( - "Failed to set vertex attribute '{Attribute}' with {ActualLength} length of array, when {ExpectedLength} was needed.", - attributeName, - attributeValues.Length, - vertices.Length); - } - return null; - } - #endregion - - #region Calculate Error - private double VertexError(ref SymmetricMatrix q, double x, double y, double z) - { - return q.m0*x*x + 2*q.m1*x*y + 2*q.m2*x*z + 2*q.m3*x + q.m4*y*y - + 2*q.m5*y*z + 2*q.m6*y + q.m7*z*z + 2*q.m8*z + q.m9; - } - - private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d result, out int resultIndex) - { - // compute interpolated vertex - SymmetricMatrix q = (vert0.q + vert1.q); - bool border = (vert0.border & vert1.border); - double error = 0.0; - double det = q.Determinant1(); - if (det != 0.0 && !border) - { - // q_delta is invertible - result = new Vector3d( - -1.0 / det * q.Determinant2(), // vx = A41/det(q_delta) - 1.0 / det * q.Determinant3(), // vy = A42/det(q_delta) - -1.0 / det * q.Determinant4()); // vz = A43/det(q_delta) - error = VertexError(ref q, result.x, result.y, result.z); - resultIndex = 2; - } - else - { - // det = 0 -> try to find best result - Vector3d p1 = vert0.p; - Vector3d p2 = vert1.p; - Vector3d p3 = (p1 + p2) * 0.5f; - double error1 = VertexError(ref q, p1.x, p1.y, p1.z); - double error2 = VertexError(ref q, p2.x, p2.y, p2.z); - double error3 = VertexError(ref q, p3.x, p3.y, p3.z); - error = MathHelper.Min(error1, error2, error3); - if (error == error3) - { - result = p3; - resultIndex = 2; - } - else if (error == error2) - { - result = p2; - resultIndex = 1; - } - else if (error == error1) - { - result = p1; - resultIndex = 0; - } - else - { - result = p3; - resultIndex = 2; - } - } - return error; - } - #endregion - - #region Flipped - /// - /// Check if a triangle flips when this edge is removed - /// - private bool Flipped(ref Vector3d p, int i0, int i1, ref Vertex v0, bool[] deleted) - { - int tcount = v0.tcount; - var refs = this.refs.Data; - var triangles = this.triangles.Data; - var vertices = this.vertices.Data; - for (int k = 0; k < tcount; k++) - { - Ref r = refs[v0.tstart + k]; - if (triangles[r.tid].deleted) - continue; - - int s = r.tvertex; - int id1 = triangles[r.tid][(s + 1) % 3]; - int id2 = triangles[r.tid][(s + 2) % 3]; - if (id1 == i1 || id2 == i1) - { - deleted[k] = true; - continue; - } - - Vector3d d1 = vertices[id1].p - p; - d1.Normalize(); - Vector3d d2 = vertices[id2].p - p; - d2.Normalize(); - double dot = Vector3d.Dot(ref d1, ref d2); - if (System.Math.Abs(dot) > 0.999) - return true; - - Vector3d n; - Vector3d.Cross(ref d1, ref d2, out n); - n.Normalize(); - deleted[k] = false; - dot = Vector3d.Dot(ref n, ref triangles[r.tid].n); - if (dot < 0.2) - return true; - } - - return false; - } - #endregion - - #region Update Triangles - /// - /// Update triangle connections and edge error after a edge is collapsed. - /// - private void UpdateTriangles(int i0, int ia0, ref Vertex v, ResizableArray deleted, ref int deletedTriangles) - { - Vector3d p; - int pIndex; - int tcount = v.tcount; - var triangles = this.triangles.Data; - var vertices = this.vertices.Data; - for (int k = 0; k < tcount; k++) - { - Ref r = refs[v.tstart + k]; - int tid = r.tid; - Triangle t = triangles[tid]; - if (t.deleted) - continue; - - if (deleted[k]) - { - triangles[tid].deleted = true; - ++deletedTriangles; - continue; - } - - t[r.tvertex] = i0; - if (ia0 != -1) - { - t.SetAttributeIndex(r.tvertex, ia0); - } - - t.dirty = true; - t.err0 = CalculateError(ref vertices[t.v0], ref vertices[t.v1], out p, out pIndex); - t.err1 = CalculateError(ref vertices[t.v1], ref vertices[t.v2], out p, out pIndex); - t.err2 = CalculateError(ref vertices[t.v2], ref vertices[t.v0], out p, out pIndex); - t.err3 = MathHelper.Min(t.err0, t.err1, t.err2); - triangles[tid] = t; - refs.Add(r); - } - } - #endregion - - #region Move/Merge Vertex Attributes - private void MoveVertexAttributes(int i0, int i1) - { - if (vertNormals != null) - { - vertNormals[i0] = vertNormals[i1]; - } - if (vertPositionWs != null) - { - vertPositionWs[i0] = vertPositionWs[i1]; - } - if (vertNormalWs != null) - { - vertNormalWs[i0] = vertNormalWs[i1]; - } - if (vertTangents != null) - { - vertTangents[i0] = vertTangents[i1]; - } - if (vertTangents2 != null) - { - vertTangents2[i0] = vertTangents2[i1]; - } - if (vertUV2D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV2D[i]; - if (vertUV != null) - { - vertUV[i0] = vertUV[i1]; - } - } - } - if (vertUV3D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV3D[i]; - if (vertUV != null) - { - vertUV[i0] = vertUV[i1]; - } - } - } - if (vertUV4D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV4D[i]; - if (vertUV != null) - { - vertUV[i0] = vertUV[i1]; - } - } - } - if (vertColors != null) - { - vertColors[i0] = vertColors[i1]; - } - if (vertBoneWeights != null) - { - vertBoneWeights[i0] = vertBoneWeights[i1]; - } - } - - private void MergeVertexAttributes(int i0, int i1) - { - if (vertNormals != null) - { - vertNormals[i0] = (vertNormals[i0] + vertNormals[i1]) * 0.5f; - } - if (vertPositionWs != null) - { - vertPositionWs[i0] = (vertPositionWs[i0] + vertPositionWs[i1]) * 0.5f; - } - if (vertNormalWs != null) - { - vertNormalWs[i0] = (vertNormalWs[i0] + vertNormalWs[i1]) * 0.5f; - } - if (vertTangents != null) - { - vertTangents[i0] = (vertTangents[i0] + vertTangents[i1]) * 0.5f; - } - if (vertTangents2 != null) - { - vertTangents2[i0] = (vertTangents2[i0] + vertTangents2[i1]) * 0.5f; - } - if (vertUV2D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV2D[i]; - if (vertUV != null) - { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; - } - } - } - if (vertUV3D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV3D[i]; - if (vertUV != null) - { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; - } - } - } - if (vertUV4D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV4D[i]; - if (vertUV != null) - { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; - } - } - } - if (vertColors != null) - { - vertColors[i0] = (vertColors[i0] + vertColors[i1]) * 0.5f; - } - - // TODO: Do we have to blend bone weights at all or can we just keep them as it is in this scenario? - } - #endregion - - #region Are UVs The Same - private bool AreUVsTheSame(int channel, int indexA, int indexB) - { - if (vertUV2D != null) - { - var vertUV = vertUV2D[channel]; - if (vertUV != null) - { - var uvA = vertUV[indexA]; - var uvB = vertUV[indexB]; - return uvA == uvB; - } - } - - if (vertUV3D != null) - { - var vertUV = vertUV3D[channel]; - if (vertUV != null) - { - var uvA = vertUV[indexA]; - var uvB = vertUV[indexB]; - return uvA == uvB; - } - } - - if (vertUV4D != null) - { - var vertUV = vertUV4D[channel]; - if (vertUV != null) - { - var uvA = vertUV[indexA]; - var uvB = vertUV[indexB]; - return uvA == uvB; - } - } - - return false; - } - #endregion - - #region Remove Vertex Pass - /// - /// Remove vertices and mark deleted triangles - /// - private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double threshold, ResizableArray deleted0, ResizableArray deleted1, ref int deletedTris) - { - var triangles = this.triangles.Data; - int triangleCount = this.triangles.Length; - var vertices = this.vertices.Data; - - bool preserveBorders = base.PreserveBorders; - int maxVertexCount = base.MaxVertexCount; - if (maxVertexCount <= 0) - maxVertexCount = int.MaxValue; - - Vector3d p; - int pIndex; - for (int tid = 0; tid < triangleCount; tid++) - { - if (triangles[tid].dirty || triangles[tid].deleted || triangles[tid].err3 > threshold) - continue; - - triangles[tid].GetErrors(errArr); - triangles[tid].GetAttributeIndices(attributeIndexArr); - for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++) - { - if (errArr[edgeIndex] > threshold) - continue; - - int nextEdgeIndex = ((edgeIndex + 1) % 3); - int i0 = triangles[tid][edgeIndex]; - int i1 = triangles[tid][nextEdgeIndex]; - - // Border check - if (vertices[i0].border != vertices[i1].border) - continue; - // Seam check - else if (vertices[i0].seam != vertices[i1].seam) - continue; - // Foldover check - else if (vertices[i0].foldover != vertices[i1].foldover) - continue; - // If borders should be preserved - else if (preserveBorders && vertices[i0].border) - continue; - // If seams should be preserved - else if (preserveSeams && vertices[i0].seam) - continue; - // If foldovers should be preserved - else if (preserveFoldovers && vertices[i0].foldover) - continue; - - // Compute vertex to collapse to - CalculateError(ref vertices[i0], ref vertices[i1], out p, out pIndex); - deleted0.Resize(vertices[i0].tcount); // normals temporarily - deleted1.Resize(vertices[i1].tcount); // normals temporarily - - // Don't remove if flipped - if (Flipped(ref p, i0, i1, ref vertices[i0], deleted0.Data)) - continue; - if (Flipped(ref p, i1, i0, ref vertices[i1], deleted1.Data)) - continue; - - int ia0 = attributeIndexArr[edgeIndex]; - - // Not flipped, so remove edge - vertices[i0].p = p; - vertices[i0].q += vertices[i1].q; - - if (pIndex == 1) - { - // Move vertex attributes from ia1 to ia0 - int ia1 = attributeIndexArr[nextEdgeIndex]; - MoveVertexAttributes(ia0, ia1); - } - else if (pIndex == 2) - { - // Merge vertex attributes ia0 and ia1 into ia0 - int ia1 = attributeIndexArr[nextEdgeIndex]; - MergeVertexAttributes(ia0, ia1); - } - - if (vertices[i0].seam) - { - ia0 = -1; - } - - int tstart = refs.Length; - UpdateTriangles(i0, ia0, ref vertices[i0], deleted0, ref deletedTris); - UpdateTriangles(i0, ia0, ref vertices[i1], deleted1, ref deletedTris); - - int tcount = refs.Length - tstart; - if (tcount <= vertices[i0].tcount) - { - // save ram - if (tcount > 0) - { - var refsArr = refs.Data; - Array.Copy(refsArr, tstart, refsArr, vertices[i0].tstart, tcount); - } - } - else - { - // append - vertices[i0].tstart = tstart; - } - - vertices[i0].tcount = tcount; - --remainingVertices; - break; - } - - // Check if we are already done - if ((startTrisCount - deletedTris) <= targetTrisCount && remainingVertices < maxVertexCount) - break; - } - } - #endregion - - #region Update Mesh - /// - /// Compact triangles, compute edge error and build reference list. - /// - /// The iteration index. - private void UpdateMesh(int iteration) - { - var triangles = this.triangles.Data; - var vertices = this.vertices.Data; - - int triangleCount = this.triangles.Length; - int vertexCount = this.vertices.Length; - if (iteration > 0) // compact triangles - { - int dst = 0; - for (int i = 0; i < triangleCount; i++) - { - if (!triangles[i].deleted) - { - if (dst != i) - { - triangles[dst] = triangles[i]; - } - dst++; - } - } - this.triangles.Resize(dst); - triangles = this.triangles.Data; - triangleCount = dst; - } - - UpdateReferences(); - - // Identify boundary : vertices[].border=0,1 - if (iteration == 0) - { - var refs = this.refs.Data; - - var vcount = new List(8); - var vids = new List(8); - int vsize = 0; - for (int i = 0; i < vertexCount; i++) - { - vertices[i].border = false; - vertices[i].seam = false; - vertices[i].foldover = false; - } - - int ofs; - int id; - int borderVertexCount = 0; - double borderMinX = double.MaxValue; - double borderMaxX = double.MinValue; - for (int i = 0; i < vertexCount; i++) - { - int tstart = vertices[i].tstart; - int tcount = vertices[i].tcount; - vcount.Clear(); - vids.Clear(); - vsize = 0; - - for (int j = 0; j < tcount; j++) - { - int tid = refs[tstart + j].tid; - for (int k = 0; k < 3; k++) - { - ofs = 0; - id = triangles[tid][k]; - while (ofs < vsize) - { - if (vids[ofs] == id) - break; - - ++ofs; - } - - if (ofs == vsize) - { - vcount.Add(1); - vids.Add(id); - ++vsize; - } - else - { - ++vcount[ofs]; - } - } - } - - for (int j = 0; j < vsize; j++) - { - if (vcount[j] == 1) - { - id = vids[j]; - vertices[id].border = true; - ++borderVertexCount; - - if (enableSmartLink) - { - if (vertices[id].p.x < borderMinX) - { - borderMinX = vertices[id].p.x; - } - if (vertices[id].p.x > borderMaxX) - { - borderMaxX = vertices[id].p.x; - } - } - } - } - } - - if (enableSmartLink) - { - // First find all border vertices - var borderVertices = new BorderVertex[borderVertexCount]; - int borderIndexCount = 0; - double borderAreaWidth = borderMaxX - borderMinX; - for (int i = 0; i < vertexCount; i++) - { - if (vertices[i].border) - { - int vertexHash = (int)(((((vertices[i].p.x - borderMinX) / borderAreaWidth) * 2.0) - 1.0) * int.MaxValue); - borderVertices[borderIndexCount] = new BorderVertex(i, vertexHash); - ++borderIndexCount; - } - } - - // Sort the border vertices by hash - Array.Sort(borderVertices, 0, borderIndexCount, BorderVertexComparer.instance); - - // Calculate the maximum hash distance based on the maximum vertex link distance - double vertexLinkDistance = System.Math.Sqrt(vertexLinkDistanceSqr); - int hashMaxDistance = System.Math.Max((int)((vertexLinkDistance / borderAreaWidth) * int.MaxValue), 1); - - // Then find identical border vertices and bind them together as one - for (int i = 0; i < borderIndexCount; i++) - { - int myIndex = borderVertices[i].index; - if (myIndex == -1) - continue; - - var myPoint = vertices[myIndex].p; - for (int j = i + 1; j < borderIndexCount; j++) - { - int otherIndex = borderVertices[j].index; - if (otherIndex == -1) - continue; - else if ((borderVertices[j].hash - borderVertices[i].hash) > hashMaxDistance) // There is no point to continue beyond this point - break; - - var otherPoint = vertices[otherIndex].p; - var sqrX = ((myPoint.x - otherPoint.x) * (myPoint.x - otherPoint.x)); - var sqrY = ((myPoint.y - otherPoint.y) * (myPoint.y - otherPoint.y)); - var sqrZ = ((myPoint.z - otherPoint.z) * (myPoint.z - otherPoint.z)); - var sqrMagnitude = sqrX + sqrY + sqrZ; - - if (sqrMagnitude <= vertexLinkDistanceSqr) - { - borderVertices[j].index = -1; // NOTE: This makes sure that the "other" vertex is not processed again - vertices[myIndex].border = false; - vertices[otherIndex].border = false; - - if (AreUVsTheSame(0, myIndex, otherIndex)) - { - vertices[myIndex].foldover = true; - vertices[otherIndex].foldover = true; - } - else - { - vertices[myIndex].seam = true; - vertices[otherIndex].seam = true; - } - - int otherTriangleCount = vertices[otherIndex].tcount; - int otherTriangleStart = vertices[otherIndex].tstart; - for (int k = 0; k < otherTriangleCount; k++) - { - var r = refs[otherTriangleStart + k]; - triangles[r.tid][r.tvertex] = myIndex; - } - } - } - } - - // Update the references again - UpdateReferences(); - } - - // Init Quadrics by Plane & Edge Errors - // - // required at the beginning ( iteration == 0 ) - // recomputing during the simplification is not required, - // but mostly improves the result for closed meshes - for (int i = 0; i < vertexCount; i++) - { - vertices[i].q = new SymmetricMatrix(); - } - - int v0, v1, v2; - Vector3d n, p0, p1, p2, p10, p20, dummy; - int dummy2; - SymmetricMatrix sm; - for (int i = 0; i < triangleCount; i++) - { - v0 = triangles[i].v0; - v1 = triangles[i].v1; - v2 = triangles[i].v2; - - p0 = vertices[v0].p; - p1 = vertices[v1].p; - p2 = vertices[v2].p; - p10 = p1 - p0; - p20 = p2 - p0; - Vector3d.Cross(ref p10, ref p20, out n); - n.Normalize(); - triangles[i].n = n; - - sm = new SymmetricMatrix(n.x, n.y, n.z, -Vector3d.Dot(ref n, ref p0)); - vertices[v0].q += sm; - vertices[v1].q += sm; - vertices[v2].q += sm; - } - - for (int i = 0; i < triangleCount; i++) - { - // Calc Edge Error - var triangle = triangles[i]; - triangles[i].err0 = CalculateError(ref vertices[triangle.v0], ref vertices[triangle.v1], out dummy, out dummy2); - triangles[i].err1 = CalculateError(ref vertices[triangle.v1], ref vertices[triangle.v2], out dummy, out dummy2); - triangles[i].err2 = CalculateError(ref vertices[triangle.v2], ref vertices[triangle.v0], out dummy, out dummy2); - triangles[i].err3 = MathHelper.Min(triangles[i].err0, triangles[i].err1, triangles[i].err2); - } - } - } - #endregion - - #region Update References - private void UpdateReferences() - { - int triangleCount = this.triangles.Length; - int vertexCount = this.vertices.Length; - var triangles = this.triangles.Data; - var vertices = this.vertices.Data; - - // Init Reference ID list - for (int i = 0; i < vertexCount; i++) - { - vertices[i].tstart = 0; - vertices[i].tcount = 0; - } - - for (int i = 0; i < triangleCount; i++) - { - ++vertices[triangles[i].v0].tcount; - ++vertices[triangles[i].v1].tcount; - ++vertices[triangles[i].v2].tcount; - } - - int tstart = 0; - remainingVertices = 0; - for (int i = 0; i < vertexCount; i++) - { - vertices[i].tstart = tstart; - if (vertices[i].tcount > 0) - { - tstart += vertices[i].tcount; - vertices[i].tcount = 0; - ++remainingVertices; - } - } - - // Write References - this.refs.Resize(tstart); - var refs = this.refs.Data; - for (int i = 0; i < triangleCount; i++) - { - int v0 = triangles[i].v0; - int v1 = triangles[i].v1; - int v2 = triangles[i].v2; - int start0 = vertices[v0].tstart; - int count0 = vertices[v0].tcount; - int start1 = vertices[v1].tstart; - int count1 = vertices[v1].tcount; - int start2 = vertices[v2].tstart; - int count2 = vertices[v2].tcount; - - refs[start0 + count0].Set(i, 0); - refs[start1 + count1].Set(i, 1); - refs[start2 + count2].Set(i, 2); - - ++vertices[v0].tcount; - ++vertices[v1].tcount; - ++vertices[v2].tcount; - } - } - #endregion - - #region Compact Mesh - /// - /// Finally compact mesh before exiting. - /// - private void CompactMesh() - { - int dst = 0; - var vertices = this.vertices.Data; - int vertexCount = this.vertices.Length; - for (int i = 0; i < vertexCount; i++) - { - vertices[i].tcount = 0; - } - - var vertNormals = (this.vertNormals != null ? this.vertNormals.Data : null); - var vertTangents = (this.vertTangents != null ? this.vertTangents.Data : null); - var vertTangents2 = (this.vertTangents2 != null ? this.vertTangents2.Data : null); - var vertUV2D = (this.vertUV2D != null ? this.vertUV2D.Data : null); - var vertUV3D = (this.vertUV3D != null ? this.vertUV3D.Data : null); - var vertUV4D = (this.vertUV4D != null ? this.vertUV4D.Data : null); - var vertColors = (this.vertColors != null ? this.vertColors.Data : null); - var vertBoneWeights = (this.vertBoneWeights != null ? this.vertBoneWeights.Data : null); - var vertPositionWs = (this.vertPositionWs != null ? this.vertPositionWs.Data : null); - var vertNormalWs = (this.vertNormalWs != null ? this.vertNormalWs.Data : null); - - var triangles = this.triangles.Data; - int triangleCount = this.triangles.Length; - for (int i = 0; i < triangleCount; i++) - { - var triangle = triangles[i]; - if (!triangle.deleted) - { - if (triangle.va0 != triangle.v0) - { - int iDest = triangle.va0; - int iSrc = triangle.v0; - vertices[iDest].p = vertices[iSrc].p; - if (vertBoneWeights != null) - { - vertBoneWeights[iDest] = vertBoneWeights[iSrc]; - } - if (vertPositionWs != null) - { - vertPositionWs[iDest] = vertPositionWs[iSrc]; - } - if (vertNormalWs != null) - { - vertNormalWs[iDest] = vertNormalWs[iSrc]; - } - triangle.v0 = triangle.va0; - } - if (triangle.va1 != triangle.v1) - { - int iDest = triangle.va1; - int iSrc = triangle.v1; - vertices[iDest].p = vertices[iSrc].p; - if (vertBoneWeights != null) - { - vertBoneWeights[iDest] = vertBoneWeights[iSrc]; - } - if (vertPositionWs != null) - { - vertPositionWs[iDest] = vertPositionWs[iSrc]; - } - if (vertNormalWs != null) - { - vertNormalWs[iDest] = vertNormalWs[iSrc]; - } - triangle.v1 = triangle.va1; - } - if (triangle.va2 != triangle.v2) - { - int iDest = triangle.va2; - int iSrc = triangle.v2; - vertices[iDest].p = vertices[iSrc].p; - if (vertBoneWeights != null) - { - vertBoneWeights[iDest] = vertBoneWeights[iSrc]; - } - if (vertPositionWs != null) - { - vertPositionWs[iDest] = vertPositionWs[iSrc]; - } - if (vertNormalWs != null) - { - vertNormalWs[iDest] = vertNormalWs[iSrc]; - } - triangle.v2 = triangle.va2; - } - - triangles[dst++] = triangle; - - vertices[triangle.v0].tcount = 1; - vertices[triangle.v1].tcount = 1; - vertices[triangle.v2].tcount = 1; - } - } - - triangleCount = dst; - this.triangles.Resize(triangleCount); - triangles = this.triangles.Data; - - dst = 0; - for (int i = 0; i < vertexCount; i++) - { - var vert = vertices[i]; - if (vert.tcount > 0) - { - vert.tstart = dst; - vertices[i] = vert; - - if (dst != i) - { - vertices[dst].p = vert.p; - if (vertNormals != null) vertNormals[dst] = vertNormals[i]; - if (vertTangents != null) vertTangents[dst] = vertTangents[i]; - if (vertTangents2 != null) vertTangents2[dst] = vertTangents2[i]; - if (vertUV2D != null) - { - for (int j = 0; j < Mesh.UVChannelCount; j++) - { - var vertUV = vertUV2D[j]; - if (vertUV != null) - { - vertUV[dst] = vertUV[i]; - } - } - } - if (vertUV3D != null) - { - for (int j = 0; j < Mesh.UVChannelCount; j++) - { - var vertUV = vertUV3D[j]; - if (vertUV != null) - { - vertUV[dst] = vertUV[i]; - } - } - } - if (vertUV4D != null) - { - for (int j = 0; j < Mesh.UVChannelCount; j++) - { - var vertUV = vertUV4D[j]; - if (vertUV != null) - { - vertUV[dst] = vertUV[i]; - } - } - } - if (vertColors != null) vertColors[dst] = vertColors[i]; - if (vertBoneWeights != null) vertBoneWeights[dst] = vertBoneWeights[i]; - if (vertPositionWs != null) vertPositionWs[dst] = vertPositionWs[i]; - if (vertNormalWs != null) vertNormalWs[dst] = vertNormalWs[i]; - } - ++dst; - } - } - - for (int i = 0; i < triangleCount; i++) - { - var triangle = triangles[i]; - triangle.v0 = vertices[triangle.v0].tstart; - triangle.v1 = vertices[triangle.v1].tstart; - triangle.v2 = vertices[triangle.v2].tstart; - triangles[i] = triangle; - } - - vertexCount = dst; - this.vertices.Resize(vertexCount); - if (vertNormals != null) this.vertNormals.Resize(vertexCount, true); - if (vertTangents != null) this.vertTangents.Resize(vertexCount, true); - if (vertTangents2 != null) this.vertTangents2.Resize(vertexCount, true); - if (vertUV2D != null) this.vertUV2D.Resize(vertexCount, true); - if (vertUV3D != null) this.vertUV3D.Resize(vertexCount, true); - if (vertUV4D != null) this.vertUV4D.Resize(vertexCount, true); - if (vertColors != null) this.vertColors.Resize(vertexCount, true); - if (vertBoneWeights != null) this.vertBoneWeights.Resize(vertexCount, true); - if (vertPositionWs != null) this.vertPositionWs.Resize(vertexCount, true); - if (vertNormalWs != null) this.vertNormalWs.Resize(vertexCount, true); - } - #endregion - #endregion - - #region Public Methods - #region Initialize - /// - /// Initializes the algorithm with the original mesh. - /// - /// The mesh. - public override void Initialize(Mesh mesh) - { - if (mesh == null) - throw new ArgumentNullException("mesh"); - - int meshSubMeshCount = mesh.SubMeshCount; - int meshTriangleCount = mesh.TriangleCount; - var meshVertices = mesh.Vertices; - var meshNormals = mesh.Normals; - var meshPositionWs = mesh.PositionWs; - var meshNormalWs = mesh.NormalWs; - var meshTangents = mesh.Tangents; - var meshTangents2 = mesh.Tangents2; - var meshColors = mesh.Colors; - var meshBoneWeights = mesh.BoneWeights; - subMeshCount = meshSubMeshCount; - - vertices.Resize(meshVertices.Length); - var vertArr = vertices.Data; - for (int i = 0; i < meshVertices.Length; i++) - { - vertArr[i] = new Vertex(meshVertices[i]); - } - - triangles.Resize(meshTriangleCount); - var trisArr = triangles.Data; - int triangleIndex = 0; - for (int subMeshIndex = 0; subMeshIndex < meshSubMeshCount; subMeshIndex++) - { - int[] subMeshIndices = mesh.GetIndices(subMeshIndex); - int subMeshTriangleCount = subMeshIndices.Length / 3; - for (int i = 0; i < subMeshTriangleCount; i++) - { - int offset = i * 3; - int v0 = subMeshIndices[offset]; - int v1 = subMeshIndices[offset + 1]; - int v2 = subMeshIndices[offset + 2]; - trisArr[triangleIndex++] = new Triangle(v0, v1, v2, subMeshIndex); - } - } - - vertNormals = InitializeVertexAttribute(meshNormals, "normals"); - vertPositionWs = InitializeVertexAttribute(meshPositionWs, "positionWs"); - vertNormalWs = InitializeVertexAttribute(meshNormalWs, "normalWs"); - vertTangents = InitializeVertexAttribute(meshTangents, "tangents"); - vertTangents2 = InitializeVertexAttribute(meshTangents2, "tangents2"); - vertColors = InitializeVertexAttribute(meshColors, "colors"); - vertBoneWeights = InitializeVertexAttribute(meshBoneWeights, "boneWeights"); - - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - int uvDim = mesh.GetUVDimension(i); - string uvAttributeName = string.Format("uv{0}", i); - if (uvDim == 2) - { - if (vertUV2D == null) - vertUV2D = new UVChannels(); - - var uvs = mesh.GetUVs2D(i); - vertUV2D[i] = InitializeVertexAttribute(uvs, uvAttributeName); - } - else if (uvDim == 3) - { - if (vertUV3D == null) - vertUV3D = new UVChannels(); - - var uvs = mesh.GetUVs3D(i); - vertUV3D[i] = InitializeVertexAttribute(uvs, uvAttributeName); - } - else if (uvDim == 4) - { - if (vertUV4D == null) - vertUV4D = new UVChannels(); - - var uvs = mesh.GetUVs4D(i); - vertUV4D[i] = InitializeVertexAttribute(uvs, uvAttributeName); - } - } - } - #endregion - - #region Decimate Mesh - /// - /// Decimates the mesh. - /// - /// The target triangle count. - public override void DecimateMesh(int targetTrisCount) - { - if (targetTrisCount < 0) - throw new ArgumentOutOfRangeException("targetTrisCount"); - - int deletedTris = 0; - ResizableArray deleted0 = new ResizableArray(20); - ResizableArray deleted1 = new ResizableArray(20); - var triangles = this.triangles.Data; - int triangleCount = this.triangles.Length; - int startTrisCount = triangleCount; - var vertices = this.vertices.Data; - - int maxVertexCount = base.MaxVertexCount; - if (maxVertexCount <= 0) - maxVertexCount = int.MaxValue; - - for (int iteration = 0; iteration < maxIterationCount; iteration++) - { - ReportStatus(iteration, startTrisCount, (startTrisCount - deletedTris), targetTrisCount); - if ((startTrisCount - deletedTris) <= targetTrisCount && remainingVertices < maxVertexCount) - break; - - // Update mesh once in a while - if ((iteration % 5) == 0) - { - UpdateMesh(iteration); - triangles = this.triangles.Data; - triangleCount = this.triangles.Length; - vertices = this.vertices.Data; - } - - // Clear dirty flag - for (int i = 0; i < triangleCount; i++) - { - triangles[i].dirty = false; - } - - // All triangles with edges below the threshold will be removed - // - // The following numbers works well for most models. - // If it does not, try to adjust the 3 parameters - double threshold = 0.000000001 * System.Math.Pow(iteration + 3, agressiveness); - - if (Verbose && (iteration % 5) == 0) - { - Logger?.LogTrace( - "Iteration {Iteration} - triangles {Triangles} threshold {Threshold}", - iteration, - (startTrisCount - deletedTris), - threshold); - } - - // Remove vertices & mark deleted triangles - RemoveVertexPass(startTrisCount, targetTrisCount, threshold, deleted0, deleted1, ref deletedTris); - } - - CompactMesh(); - } - #endregion - - #region Decimate Mesh Lossless - /// - /// Decimates the mesh without losing any quality. - /// - public override void DecimateMeshLossless() - { - int deletedTris = 0; - ResizableArray deleted0 = new ResizableArray(0); - ResizableArray deleted1 = new ResizableArray(0); - var triangles = this.triangles.Data; - int triangleCount = this.triangles.Length; - int startTrisCount = triangleCount; - var vertices = this.vertices.Data; - - ReportStatus(0, startTrisCount, startTrisCount, -1); - for (int iteration = 0; iteration < 9999; iteration++) - { - // Update mesh constantly - UpdateMesh(iteration); - triangles = this.triangles.Data; - triangleCount = this.triangles.Length; - vertices = this.vertices.Data; - - ReportStatus(iteration, startTrisCount, triangleCount, -1); - - // Clear dirty flag - for (int i = 0; i < triangleCount; i++) - { - triangles[i].dirty = false; - } - - // All triangles with edges below the threshold will be removed - // - // The following numbers works well for most models. - // If it does not, try to adjust the 3 parameters - double threshold = DoubleEpsilon; - - if (Verbose) - { - Logger?.LogTrace("Lossless iteration {Iteration}", iteration); - } - - // Remove vertices & mark deleted triangles - RemoveVertexPass(startTrisCount, 0, threshold, deleted0, deleted1, ref deletedTris); - - if (deletedTris <= 0) - break; - - deletedTris = 0; - } - - CompactMesh(); - } - #endregion - - #region To Mesh - /// - /// Returns the resulting mesh. - /// - /// The resulting mesh. - public override Mesh ToMesh() - { - int vertexCount = this.vertices.Length; - int triangleCount = this.triangles.Length; - var vertices = new Vector3d[vertexCount]; - var indices = new int[subMeshCount][]; - - var vertArr = this.vertices.Data; - for (int i = 0; i < vertexCount; i++) - { - vertices[i] = vertArr[i].p; - } - - // First get the sub-mesh offsets - var triArr = this.triangles.Data; - int[] subMeshOffsets = new int[subMeshCount]; - int lastSubMeshOffset = -1; - for (int i = 0; i < triangleCount; i++) - { - var triangle = triArr[i]; - if (triangle.subMeshIndex != lastSubMeshOffset) - { - for (int j = lastSubMeshOffset + 1; j < triangle.subMeshIndex; j++) - { - subMeshOffsets[j] = i; - } - subMeshOffsets[triangle.subMeshIndex] = i; - lastSubMeshOffset = triangle.subMeshIndex; - } - } - for (int i = lastSubMeshOffset + 1; i < subMeshCount; i++) - { - subMeshOffsets[i] = triangleCount; - } - - // Then setup the sub-meshes - for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) - { - int startOffset = subMeshOffsets[subMeshIndex]; - if (startOffset < triangleCount) - { - int endOffset = ((subMeshIndex + 1) < subMeshCount ? subMeshOffsets[subMeshIndex + 1] : triangleCount); - int subMeshTriangleCount = endOffset - startOffset; - if (subMeshTriangleCount < 0) subMeshTriangleCount = 0; - int[] subMeshIndices = new int[subMeshTriangleCount * 3]; - - for (int triangleIndex = startOffset; triangleIndex < endOffset; triangleIndex++) - { - var triangle = triArr[triangleIndex]; - int offset = (triangleIndex - startOffset) * 3; - subMeshIndices[offset] = triangle.v0; - subMeshIndices[offset + 1] = triangle.v1; - subMeshIndices[offset + 2] = triangle.v2; - } - - indices[subMeshIndex] = subMeshIndices; - } - else - { - // This mesh doesn't have any triangles left - indices[subMeshIndex] = new int[0]; - } - } - - Mesh newMesh = new Mesh(vertices, indices); - - if (vertNormals != null) - { - newMesh.Normals = vertNormals.Data; - } - if (vertPositionWs != null) - { - newMesh.PositionWs = vertPositionWs.Data; - } - if (vertNormalWs != null) - { - newMesh.NormalWs = vertNormalWs.Data; - } - if (vertTangents != null) - { - newMesh.Tangents = vertTangents.Data; - } - if (vertTangents2 != null) - { - newMesh.Tangents2 = vertTangents2.Data; - } - if (vertColors != null) - { - newMesh.Colors = vertColors.Data; - } - if (vertBoneWeights != null) - { - newMesh.BoneWeights = vertBoneWeights.Data; - } - - if (vertUV2D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - if (vertUV2D[i] != null) - { - var uvSet = vertUV2D[i].Data; - newMesh.SetUVs(i, uvSet); - } - } - } - - if (vertUV3D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - if (vertUV3D[i] != null) - { - var uvSet = vertUV3D[i].Data; - newMesh.SetUVs(i, uvSet); - } - } - } - - if (vertUV4D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - if (vertUV4D[i] != null) - { - var uvSet = vertUV4D[i].Data; - newMesh.SetUVs(i, uvSet); - } - } - } - - return newMesh; - } - #endregion - #endregion - } -} diff --git a/LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs b/LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs deleted file mode 100644 index 6501468..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs +++ /dev/null @@ -1,249 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using MeshDecimator.Math; - -namespace MeshDecimator -{ - /// - /// A bone weight. - /// - public struct BoneWeight : IEquatable - { - #region Fields - /// - /// The first bone index. - /// - public int boneIndex0; - /// - /// The second bone index. - /// - public int boneIndex1; - /// - /// The third bone index. - /// - public int boneIndex2; - /// - /// The fourth bone index. - /// - public int boneIndex3; - - /// - /// The first bone weight. - /// - public float boneWeight0; - /// - /// The second bone weight. - /// - public float boneWeight1; - /// - /// The third bone weight. - /// - public float boneWeight2; - /// - /// The fourth bone weight. - /// - public float boneWeight3; - #endregion - - #region Constructor - /// - /// Creates a new bone weight. - /// - /// The first bone index. - /// The second bone index. - /// The third bone index. - /// The fourth bone index. - /// The first bone weight. - /// The second bone weight. - /// The third bone weight. - /// The fourth bone weight. - public BoneWeight(int boneIndex0, int boneIndex1, int boneIndex2, int boneIndex3, float boneWeight0, float boneWeight1, float boneWeight2, float boneWeight3) - { - this.boneIndex0 = boneIndex0; - this.boneIndex1 = boneIndex1; - this.boneIndex2 = boneIndex2; - this.boneIndex3 = boneIndex3; - - this.boneWeight0 = boneWeight0; - this.boneWeight1 = boneWeight1; - this.boneWeight2 = boneWeight2; - this.boneWeight3 = boneWeight3; - } - #endregion - - #region Operators - /// - /// Returns if two bone weights equals eachother. - /// - /// The left hand side bone weight. - /// The right hand side bone weight. - /// If equals. - public static bool operator ==(BoneWeight lhs, BoneWeight rhs) - { - return (lhs.boneIndex0 == rhs.boneIndex0 && lhs.boneIndex1 == rhs.boneIndex1 && lhs.boneIndex2 == rhs.boneIndex2 && lhs.boneIndex3 == rhs.boneIndex3 && - new Vector4(lhs.boneWeight0, lhs.boneWeight1, lhs.boneWeight2, lhs.boneWeight3) == new Vector4(rhs.boneWeight0, rhs.boneWeight1, rhs.boneWeight2, rhs.boneWeight3)); - } - - /// - /// Returns if two bone weights don't equal eachother. - /// - /// The left hand side bone weight. - /// The right hand side bone weight. - /// If not equals. - public static bool operator !=(BoneWeight lhs, BoneWeight rhs) - { - return !(lhs == rhs); - } - #endregion - - #region Private Methods - private void MergeBoneWeight(int boneIndex, float weight) - { - if (boneIndex == boneIndex0) - { - boneWeight0 = (boneWeight0 + weight) * 0.5f; - } - else if (boneIndex == boneIndex1) - { - boneWeight1 = (boneWeight1 + weight) * 0.5f; - } - else if (boneIndex == boneIndex2) - { - boneWeight2 = (boneWeight2 + weight) * 0.5f; - } - else if (boneIndex == boneIndex3) - { - boneWeight3 = (boneWeight3 + weight) * 0.5f; - } - else if(boneWeight0 == 0f) - { - boneIndex0 = boneIndex; - boneWeight0 = weight; - } - else if (boneWeight1 == 0f) - { - boneIndex1 = boneIndex; - boneWeight1 = weight; - } - else if (boneWeight2 == 0f) - { - boneIndex2 = boneIndex; - boneWeight2 = weight; - } - else if (boneWeight3 == 0f) - { - boneIndex3 = boneIndex; - boneWeight3 = weight; - } - Normalize(); - } - - private void Normalize() - { - float mag = (float)System.Math.Sqrt(boneWeight0 * boneWeight0 + boneWeight1 * boneWeight1 + boneWeight2 * boneWeight2 + boneWeight3 * boneWeight3); - if (mag > float.Epsilon) - { - boneWeight0 /= mag; - boneWeight1 /= mag; - boneWeight2 /= mag; - boneWeight3 /= mag; - } - else - { - boneWeight0 = boneWeight1 = boneWeight2 = boneWeight3 = 0f; - } - } - #endregion - - #region Public Methods - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return boneIndex0.GetHashCode() ^ boneIndex1.GetHashCode() << 2 ^ boneIndex2.GetHashCode() >> 2 ^ boneIndex3.GetHashCode() >> - 1 ^ boneWeight0.GetHashCode() << 5 ^ boneWeight1.GetHashCode() << 4 ^ boneWeight2.GetHashCode() >> 4 ^ boneWeight3.GetHashCode() >> 3; - } - - /// - /// Returns if this bone weight is equal to another object. - /// - /// The other object to compare to. - /// If equals. - public override bool Equals(object obj) - { - if (!(obj is BoneWeight)) - { - return false; - } - BoneWeight other = (BoneWeight)obj; - return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 && - boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3); - } - - /// - /// Returns if this bone weight is equal to another one. - /// - /// The other bone weight to compare to. - /// If equals. - public bool Equals(BoneWeight other) - { - return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 && - boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3); - } - - /// - /// Returns a nicely formatted string for this bone weight. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}:{4:F1}, {1}:{5:F1}, {2}:{6:F1}, {3}:{7:F1})", - boneIndex0, boneIndex1, boneIndex2, boneIndex3, boneWeight0, boneWeight1, boneWeight2, boneWeight3); - } - #endregion - - #region Static - /// - /// Merges two bone weights and stores the merged result in the first parameter. - /// - /// The first bone weight, also stores result. - /// The second bone weight. - public static void Merge(ref BoneWeight a, ref BoneWeight b) - { - if (b.boneWeight0 > 0f) a.MergeBoneWeight(b.boneIndex0, b.boneWeight0); - if (b.boneWeight1 > 0f) a.MergeBoneWeight(b.boneIndex1, b.boneWeight1); - if (b.boneWeight2 > 0f) a.MergeBoneWeight(b.boneIndex2, b.boneWeight2); - if (b.boneWeight3 > 0f) a.MergeBoneWeight(b.boneIndex3, b.boneWeight3); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs b/LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs deleted file mode 100644 index 2c69814..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs +++ /dev/null @@ -1,179 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; - -namespace MeshDecimator.Collections -{ - /// - /// A resizable array. - /// - /// The item type. - internal sealed class ResizableArray - { - #region Fields - private T[] items = null; - private int length = 0; - - private static T[] emptyArr = new T[0]; - #endregion - - #region Properties - /// - /// Gets the length of this array. - /// - public int Length - { - get { return length; } - } - - /// - /// Gets the internal data buffer for this array. - /// - public T[] Data - { - get { return items; } - } - - /// - /// Gets or sets the element value at a specific index. - /// - /// The element index. - /// The element value. - public T this[int index] - { - get { return items[index]; } - set { items[index] = value; } - } - #endregion - - #region Constructor - /// - /// Creates a new resizable array. - /// - /// The initial array capacity. - public ResizableArray(int capacity) - : this(capacity, 0) - { - - } - - /// - /// Creates a new resizable array. - /// - /// The initial array capacity. - /// The initial length of the array. - public ResizableArray(int capacity, int length) - { - if (capacity < 0) - throw new ArgumentOutOfRangeException("capacity"); - else if (length < 0 || length > capacity) - throw new ArgumentOutOfRangeException("length"); - - if (capacity > 0) - items = new T[capacity]; - else - items = emptyArr; - - this.length = length; - } - #endregion - - #region Private Methods - private void IncreaseCapacity(int capacity) - { - T[] newItems = new T[capacity]; - Array.Copy(items, 0, newItems, 0, System.Math.Min(length, capacity)); - items = newItems; - } - #endregion - - #region Public Methods - /// - /// Clears this array. - /// - public void Clear() - { - Array.Clear(items, 0, length); - length = 0; - } - - /// - /// Resizes this array. - /// - /// The new length. - /// If exess memory should be trimmed. - public void Resize(int length, bool trimExess = false) - { - if (length < 0) - throw new ArgumentOutOfRangeException("capacity"); - - if (length > items.Length) - { - IncreaseCapacity(length); - } - else if (length < this.length) - { - //Array.Clear(items, capacity, length - capacity); - } - - this.length = length; - - if (trimExess) - { - TrimExcess(); - } - } - - /// - /// Trims any excess memory for this array. - /// - public void TrimExcess() - { - if (items.Length == length) // Nothing to do - return; - - T[] newItems = new T[length]; - Array.Copy(items, 0, newItems, 0, length); - items = newItems; - } - - /// - /// Adds a new item to the end of this array. - /// - /// The new item. - public void Add(T item) - { - if (length >= items.Length) - { - IncreaseCapacity(items.Length << 1); - } - - items[length++] = item; - } - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs b/LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs deleted file mode 100644 index 073728a..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; - -namespace MeshDecimator.Collections -{ - /// - /// A collection of UV channels. - /// - /// The UV vector type. - internal sealed class UVChannels - { - #region Fields - private ResizableArray[] channels = null; - private TVec[][] channelsData = null; - #endregion - - #region Properties - /// - /// Gets the channel collection data. - /// - public TVec[][] Data - { - get - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - if (channels[i] != null) - { - channelsData[i] = channels[i].Data; - } - else - { - channelsData[i] = null; - } - } - return channelsData; - } - } - - /// - /// Gets or sets a specific channel by index. - /// - /// The channel index. - public ResizableArray this[int index] - { - get { return channels[index]; } - set { channels[index] = value; } - } - #endregion - - #region Constructor - /// - /// Creates a new collection of UV channels. - /// - public UVChannels() - { - channels = new ResizableArray[Mesh.UVChannelCount]; - channelsData = new TVec[Mesh.UVChannelCount][]; - } - #endregion - - #region Public Methods - /// - /// Resizes all channels at once. - /// - /// The new capacity. - /// If exess memory should be trimmed. - public void Resize(int capacity, bool trimExess = false) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - if (channels[i] != null) - { - channels[i].Resize(capacity, trimExess); - } - } - } - #endregion - } -} diff --git a/LightlessSync/ThirdParty/MeshDecimator/LICENSE.md b/LightlessSync/ThirdParty/MeshDecimator/LICENSE.md deleted file mode 100644 index 1f1f192..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs deleted file mode 100644 index b530d3d..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs +++ /dev/null @@ -1,286 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; - -namespace MeshDecimator.Math -{ - /// - /// Math helpers. - /// - public static class MathHelper - { - #region Consts - /// - /// The Pi constant. - /// - public const float PI = 3.14159274f; - - /// - /// The Pi constant. - /// - public const double PId = 3.1415926535897932384626433832795; - - /// - /// Degrees to radian constant. - /// - public const float Deg2Rad = PI / 180f; - - /// - /// Degrees to radian constant. - /// - public const double Deg2Radd = PId / 180.0; - - /// - /// Radians to degrees constant. - /// - public const float Rad2Deg = 180f / PI; - - /// - /// Radians to degrees constant. - /// - public const double Rad2Degd = 180.0 / PId; - #endregion - - #region Min - /// - /// Returns the minimum of two values. - /// - /// The first value. - /// The second value. - /// The minimum value. - public static int Min(int val1, int val2) - { - return (val1 < val2 ? val1 : val2); - } - - /// - /// Returns the minimum of three values. - /// - /// The first value. - /// The second value. - /// The third value. - /// The minimum value. - public static int Min(int val1, int val2, int val3) - { - return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); - } - - /// - /// Returns the minimum of two values. - /// - /// The first value. - /// The second value. - /// The minimum value. - public static float Min(float val1, float val2) - { - return (val1 < val2 ? val1 : val2); - } - - /// - /// Returns the minimum of three values. - /// - /// The first value. - /// The second value. - /// The third value. - /// The minimum value. - public static float Min(float val1, float val2, float val3) - { - return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); - } - - /// - /// Returns the minimum of two values. - /// - /// The first value. - /// The second value. - /// The minimum value. - public static double Min(double val1, double val2) - { - return (val1 < val2 ? val1 : val2); - } - - /// - /// Returns the minimum of three values. - /// - /// The first value. - /// The second value. - /// The third value. - /// The minimum value. - public static double Min(double val1, double val2, double val3) - { - return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); - } - #endregion - - #region Max - /// - /// Returns the maximum of two values. - /// - /// The first value. - /// The second value. - /// The maximum value. - public static int Max(int val1, int val2) - { - return (val1 > val2 ? val1 : val2); - } - - /// - /// Returns the maximum of three values. - /// - /// The first value. - /// The second value. - /// The third value. - /// The maximum value. - public static int Max(int val1, int val2, int val3) - { - return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3)); - } - - /// - /// Returns the maximum of two values. - /// - /// The first value. - /// The second value. - /// The maximum value. - public static float Max(float val1, float val2) - { - return (val1 > val2 ? val1 : val2); - } - - /// - /// Returns the maximum of three values. - /// - /// The first value. - /// The second value. - /// The third value. - /// The maximum value. - public static float Max(float val1, float val2, float val3) - { - return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3)); - } - - /// - /// Returns the maximum of two values. - /// - /// The first value. - /// The second value. - /// The maximum value. - public static double Max(double val1, double val2) - { - return (val1 > val2 ? val1 : val2); - } - - /// - /// Returns the maximum of three values. - /// - /// The first value. - /// The second value. - /// The third value. - /// The maximum value. - public static double Max(double val1, double val2, double val3) - { - return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3)); - } - #endregion - - #region Clamping - /// - /// Clamps a value between a minimum and a maximum value. - /// - /// The value to clamp. - /// The minimum value. - /// The maximum value. - /// The clamped value. - public static float Clamp(float value, float min, float max) - { - return (value >= min ? (value <= max ? value : max) : min); - } - - /// - /// Clamps a value between a minimum and a maximum value. - /// - /// The value to clamp. - /// The minimum value. - /// The maximum value. - /// The clamped value. - public static double Clamp(double value, double min, double max) - { - return (value >= min ? (value <= max ? value : max) : min); - } - - /// - /// Clamps the value between 0 and 1. - /// - /// The value to clamp. - /// The clamped value. - public static float Clamp01(float value) - { - return (value > 0f ? (value < 1f ? value : 1f) : 0f); - } - - /// - /// Clamps the value between 0 and 1. - /// - /// The value to clamp. - /// The clamped value. - public static double Clamp01(double value) - { - return (value > 0.0 ? (value < 1.0 ? value : 1.0) : 0.0); - } - #endregion - - #region Triangle Area - /// - /// Calculates the area of a triangle. - /// - /// The first point. - /// The second point. - /// The third point. - /// The triangle area. - public static float TriangleArea(ref Vector3 p0, ref Vector3 p1, ref Vector3 p2) - { - var dx = p1 - p0; - var dy = p2 - p0; - return dx.Magnitude * ((float)System.Math.Sin(Vector3.Angle(ref dx, ref dy) * Deg2Rad) * dy.Magnitude) * 0.5f; - } - - /// - /// Calculates the area of a triangle. - /// - /// The first point. - /// The second point. - /// The third point. - /// The triangle area. - public static double TriangleArea(ref Vector3d p0, ref Vector3d p1, ref Vector3d p2) - { - var dx = p1 - p0; - var dy = p2 - p0; - return dx.Magnitude * (System.Math.Sin(Vector3d.Angle(ref dx, ref dy) * Deg2Radd) * dy.Magnitude) * 0.5f; - } - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs deleted file mode 100644 index 3daa4e7..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs +++ /dev/null @@ -1,303 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; - -namespace MeshDecimator.Math -{ - /// - /// A symmetric matrix. - /// - public struct SymmetricMatrix - { - #region Fields - /// - /// The m11 component. - /// - public double m0; - /// - /// The m12 component. - /// - public double m1; - /// - /// The m13 component. - /// - public double m2; - /// - /// The m14 component. - /// - public double m3; - /// - /// The m22 component. - /// - public double m4; - /// - /// The m23 component. - /// - public double m5; - /// - /// The m24 component. - /// - public double m6; - /// - /// The m33 component. - /// - public double m7; - /// - /// The m34 component. - /// - public double m8; - /// - /// The m44 component. - /// - public double m9; - #endregion - - #region Properties - /// - /// Gets the component value with a specific index. - /// - /// The component index. - /// The value. - public double this[int index] - { - get - { - switch (index) - { - case 0: - return m0; - case 1: - return m1; - case 2: - return m2; - case 3: - return m3; - case 4: - return m4; - case 5: - return m5; - case 6: - return m6; - case 7: - return m7; - case 8: - return m8; - case 9: - return m9; - default: - throw new IndexOutOfRangeException(); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a symmetric matrix with a value in each component. - /// - /// The component value. - public SymmetricMatrix(double c) - { - this.m0 = c; - this.m1 = c; - this.m2 = c; - this.m3 = c; - this.m4 = c; - this.m5 = c; - this.m6 = c; - this.m7 = c; - this.m8 = c; - this.m9 = c; - } - - /// - /// Creates a symmetric matrix. - /// - /// The m11 component. - /// The m12 component. - /// The m13 component. - /// The m14 component. - /// The m22 component. - /// The m23 component. - /// The m24 component. - /// The m33 component. - /// The m34 component. - /// The m44 component. - public SymmetricMatrix(double m0, double m1, double m2, double m3, - double m4, double m5, double m6, double m7, double m8, double m9) - { - this.m0 = m0; - this.m1 = m1; - this.m2 = m2; - this.m3 = m3; - this.m4 = m4; - this.m5 = m5; - this.m6 = m6; - this.m7 = m7; - this.m8 = m8; - this.m9 = m9; - } - - /// - /// Creates a symmetric matrix from a plane. - /// - /// The plane x-component. - /// The plane y-component - /// The plane z-component - /// The plane w-component - public SymmetricMatrix(double a, double b, double c, double d) - { - this.m0 = a * a; - this.m1 = a * b; - this.m2 = a * c; - this.m3 = a * d; - - this.m4 = b * b; - this.m5 = b * c; - this.m6 = b * d; - - this.m7 = c * c; - this.m8 = c * d; - - this.m9 = d * d; - } - #endregion - - #region Operators - /// - /// Adds two matrixes together. - /// - /// The left hand side. - /// The right hand side. - /// The resulting matrix. - public static SymmetricMatrix operator +(SymmetricMatrix a, SymmetricMatrix b) - { - return new SymmetricMatrix( - a.m0 + b.m0, a.m1 + b.m1, a.m2 + b.m2, a.m3 + b.m3, - a.m4 + b.m4, a.m5 + b.m5, a.m6 + b.m6, - a.m7 + b.m7, a.m8 + b.m8, - a.m9 + b.m9 - ); - } - #endregion - - #region Internal Methods - /// - /// Determinant(0, 1, 2, 1, 4, 5, 2, 5, 7) - /// - /// - internal double Determinant1() - { - double det = - m0 * m4 * m7 + - m2 * m1 * m5 + - m1 * m5 * m2 - - m2 * m4 * m2 - - m0 * m5 * m5 - - m1 * m1 * m7; - return det; - } - - /// - /// Determinant(1, 2, 3, 4, 5, 6, 5, 7, 8) - /// - /// - internal double Determinant2() - { - double det = - m1 * m5 * m8 + - m3 * m4 * m7 + - m2 * m6 * m5 - - m3 * m5 * m5 - - m1 * m6 * m7 - - m2 * m4 * m8; - return det; - } - - /// - /// Determinant(0, 2, 3, 1, 5, 6, 2, 7, 8) - /// - /// - internal double Determinant3() - { - double det = - m0 * m5 * m8 + - m3 * m1 * m7 + - m2 * m6 * m2 - - m3 * m5 * m2 - - m0 * m6 * m7 - - m2 * m1 * m8; - return det; - } - - /// - /// Determinant(0, 1, 3, 1, 4, 6, 2, 5, 8) - /// - /// - internal double Determinant4() - { - double det = - m0 * m4 * m8 + - m3 * m1 * m5 + - m1 * m6 * m2 - - m3 * m4 * m2 - - m0 * m6 * m5 - - m1 * m1 * m8; - return det; - } - #endregion - - #region Public Methods - /// - /// Computes the determinant of this matrix. - /// - /// The a11 index. - /// The a12 index. - /// The a13 index. - /// The a21 index. - /// The a22 index. - /// The a23 index. - /// The a31 index. - /// The a32 index. - /// The a33 index. - /// The determinant value. - public double Determinant(int a11, int a12, int a13, - int a21, int a22, int a23, - int a31, int a32, int a33) - { - double det = - this[a11] * this[a22] * this[a33] + - this[a13] * this[a21] * this[a32] + - this[a12] * this[a23] * this[a31] - - this[a13] * this[a22] * this[a31] - - this[a11] * this[a23] * this[a32] - - this[a12] * this[a21] * this[a33]; - return det; - } - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs deleted file mode 100644 index 68f06f4..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs +++ /dev/null @@ -1,425 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A single precision 2D vector. - /// - public struct Vector2 : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector2 zero = new Vector2(0, 0); - #endregion - - #region Consts - /// - /// The vector epsilon. - /// - public const float Epsilon = 9.99999944E-11f; - #endregion - - #region Fields - /// - /// The x component. - /// - public float x; - /// - /// The y component. - /// - public float y; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public float Magnitude - { - get { return (float)System.Math.Sqrt(x * x + y * y); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public float MagnitudeSqr - { - get { return (x * x + y * y); } - } - - /// - /// Gets a normalized vector from this vector. - /// - public Vector2 Normalized - { - get - { - Vector2 result; - Normalize(ref this, out result); - return result; - } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public float this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - default: - throw new IndexOutOfRangeException("Invalid Vector2 index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector2 index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector2(float value) - { - this.x = value; - this.y = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - public Vector2(float x, float y) - { - this.x = x; - this.y = y; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector2 operator +(Vector2 a, Vector2 b) - { - return new Vector2(a.x + b.x, a.y + b.y); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector2 operator -(Vector2 a, Vector2 b) - { - return new Vector2(a.x - b.x, a.y - b.y); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector2 operator *(Vector2 a, float d) - { - return new Vector2(a.x * d, a.y * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector2 operator *(float d, Vector2 a) - { - return new Vector2(a.x * d, a.y * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector2 operator /(Vector2 a, float d) - { - return new Vector2(a.x / d, a.y / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector2 operator -(Vector2 a) - { - return new Vector2(-a.x, -a.y); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector2 lhs, Vector2 rhs) - { - return (lhs - rhs).MagnitudeSqr < Epsilon; - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector2 lhs, Vector2 rhs) - { - return (lhs - rhs).MagnitudeSqr >= Epsilon; - } - - /// - /// Explicitly converts from a double-precision vector into a single-precision vector. - /// - /// The double-precision vector. - public static explicit operator Vector2(Vector2d v) - { - return new Vector2((float)v.x, (float)v.y); - } - - /// - /// Implicitly converts from an integer vector into a single-precision vector. - /// - /// The integer vector. - public static implicit operator Vector2(Vector2i v) - { - return new Vector2(v.x, v.y); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x and y components of an existing vector. - /// - /// The x value. - /// The y value. - public void Set(float x, float y) - { - this.x = x; - this.y = y; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector2 scale) - { - x *= scale.x; - y *= scale.y; - } - - /// - /// Normalizes this vector. - /// - public void Normalize() - { - float mag = this.Magnitude; - if (mag > Epsilon) - { - x /= mag; - y /= mag; - } - else - { - x = y = 0; - } - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(float min, float max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector2)) - { - return false; - } - Vector2 vector = (Vector2)other; - return (x == vector.x && y == vector.y); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector2 other) - { - return (x == other.x && y == other.y); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1})", - x.ToString("F1", CultureInfo.InvariantCulture), - y.ToString("F1", CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The float format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Dot Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - public static float Dot(ref Vector2 lhs, ref Vector2 rhs) - { - return lhs.x * rhs.x + lhs.y * rhs.y; - } - - /// - /// Performs a linear interpolation between two vectors. - /// - /// The vector to interpolate from. - /// The vector to interpolate to. - /// The time fraction. - /// The resulting vector. - public static void Lerp(ref Vector2 a, ref Vector2 b, float t, out Vector2 result) - { - result = new Vector2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); - } - - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector2 a, ref Vector2 b, out Vector2 result) - { - result = new Vector2(a.x * b.x, a.y * b.y); - } - - /// - /// Normalizes a vector. - /// - /// The vector to normalize. - /// The resulting normalized vector. - public static void Normalize(ref Vector2 value, out Vector2 result) - { - float mag = value.Magnitude; - if (mag > Epsilon) - { - result = new Vector2(value.x / mag, value.y / mag); - } - else - { - result = Vector2.zero; - } - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs deleted file mode 100644 index 72f62aa..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs +++ /dev/null @@ -1,425 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A double precision 2D vector. - /// - public struct Vector2d : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector2d zero = new Vector2d(0, 0); - #endregion - - #region Consts - /// - /// The vector epsilon. - /// - public const double Epsilon = double.Epsilon; - #endregion - - #region Fields - /// - /// The x component. - /// - public double x; - /// - /// The y component. - /// - public double y; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public double Magnitude - { - get { return System.Math.Sqrt(x * x + y * y); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public double MagnitudeSqr - { - get { return (x * x + y * y); } - } - - /// - /// Gets a normalized vector from this vector. - /// - public Vector2d Normalized - { - get - { - Vector2d result; - Normalize(ref this, out result); - return result; - } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public double this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - default: - throw new IndexOutOfRangeException("Invalid Vector2d index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector2d index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector2d(double value) - { - this.x = value; - this.y = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - public Vector2d(double x, double y) - { - this.x = x; - this.y = y; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector2d operator +(Vector2d a, Vector2d b) - { - return new Vector2d(a.x + b.x, a.y + b.y); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector2d operator -(Vector2d a, Vector2d b) - { - return new Vector2d(a.x - b.x, a.y - b.y); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector2d operator *(Vector2d a, double d) - { - return new Vector2d(a.x * d, a.y * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector2d operator *(double d, Vector2d a) - { - return new Vector2d(a.x * d, a.y * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector2d operator /(Vector2d a, double d) - { - return new Vector2d(a.x / d, a.y / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector2d operator -(Vector2d a) - { - return new Vector2d(-a.x, -a.y); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector2d lhs, Vector2d rhs) - { - return (lhs - rhs).MagnitudeSqr < Epsilon; - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector2d lhs, Vector2d rhs) - { - return (lhs - rhs).MagnitudeSqr >= Epsilon; - } - - /// - /// Implicitly converts from a single-precision vector into a double-precision vector. - /// - /// The single-precision vector. - public static implicit operator Vector2d(Vector2 v) - { - return new Vector2d(v.x, v.y); - } - - /// - /// Implicitly converts from an integer vector into a double-precision vector. - /// - /// The integer vector. - public static implicit operator Vector2d(Vector2i v) - { - return new Vector2d(v.x, v.y); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x and y components of an existing vector. - /// - /// The x value. - /// The y value. - public void Set(double x, double y) - { - this.x = x; - this.y = y; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector2d scale) - { - x *= scale.x; - y *= scale.y; - } - - /// - /// Normalizes this vector. - /// - public void Normalize() - { - double mag = this.Magnitude; - if (mag > Epsilon) - { - x /= mag; - y /= mag; - } - else - { - x = y = 0; - } - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(double min, double max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector2d)) - { - return false; - } - Vector2d vector = (Vector2d)other; - return (x == vector.x && y == vector.y); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector2d other) - { - return (x == other.x && y == other.y); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1})", - x.ToString("F1", CultureInfo.InvariantCulture), - y.ToString("F1", CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The float format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Dot Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - public static double Dot(ref Vector2d lhs, ref Vector2d rhs) - { - return lhs.x * rhs.x + lhs.y * rhs.y; - } - - /// - /// Performs a linear interpolation between two vectors. - /// - /// The vector to interpolate from. - /// The vector to interpolate to. - /// The time fraction. - /// The resulting vector. - public static void Lerp(ref Vector2d a, ref Vector2d b, double t, out Vector2d result) - { - result = new Vector2d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); - } - - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector2d a, ref Vector2d b, out Vector2d result) - { - result = new Vector2d(a.x * b.x, a.y * b.y); - } - - /// - /// Normalizes a vector. - /// - /// The vector to normalize. - /// The resulting normalized vector. - public static void Normalize(ref Vector2d value, out Vector2d result) - { - double mag = value.Magnitude; - if (mag > Epsilon) - { - result = new Vector2d(value.x / mag, value.y / mag); - } - else - { - result = Vector2d.zero; - } - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs deleted file mode 100644 index 20b808b..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs +++ /dev/null @@ -1,348 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A 2D integer vector. - /// - public struct Vector2i : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector2i zero = new Vector2i(0, 0); - #endregion - - #region Fields - /// - /// The x component. - /// - public int x; - /// - /// The y component. - /// - public int y; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public int Magnitude - { - get { return (int)System.Math.Sqrt(x * x + y * y); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public int MagnitudeSqr - { - get { return (x * x + y * y); } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public int this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - default: - throw new IndexOutOfRangeException("Invalid Vector2i index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector2i index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector2i(int value) - { - this.x = value; - this.y = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - public Vector2i(int x, int y) - { - this.x = x; - this.y = y; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector2i operator +(Vector2i a, Vector2i b) - { - return new Vector2i(a.x + b.x, a.y + b.y); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector2i operator -(Vector2i a, Vector2i b) - { - return new Vector2i(a.x - b.x, a.y - b.y); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector2i operator *(Vector2i a, int d) - { - return new Vector2i(a.x * d, a.y * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector2i operator *(int d, Vector2i a) - { - return new Vector2i(a.x * d, a.y * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector2i operator /(Vector2i a, int d) - { - return new Vector2i(a.x / d, a.y / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector2i operator -(Vector2i a) - { - return new Vector2i(-a.x, -a.y); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector2i lhs, Vector2i rhs) - { - return (lhs.x == rhs.x && lhs.y == rhs.y); - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector2i lhs, Vector2i rhs) - { - return (lhs.x != rhs.x || lhs.y != rhs.y); - } - - /// - /// Explicitly converts from a single-precision vector into an integer vector. - /// - /// The single-precision vector. - public static explicit operator Vector2i(Vector2 v) - { - return new Vector2i((int)v.x, (int)v.y); - } - - /// - /// Explicitly converts from a double-precision vector into an integer vector. - /// - /// The double-precision vector. - public static explicit operator Vector2i(Vector2d v) - { - return new Vector2i((int)v.x, (int)v.y); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x and y components of an existing vector. - /// - /// The x value. - /// The y value. - public void Set(int x, int y) - { - this.x = x; - this.y = y; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector2i scale) - { - x *= scale.x; - y *= scale.y; - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(int min, int max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector2i)) - { - return false; - } - Vector2i vector = (Vector2i)other; - return (x == vector.x && y == vector.y); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector2i other) - { - return (x == other.x && y == other.y); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1})", - x.ToString(CultureInfo.InvariantCulture), - y.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The integer format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector2i a, ref Vector2i b, out Vector2i result) - { - result = new Vector2i(a.x * b.x, a.y * b.y); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs deleted file mode 100644 index 4c91aa5..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs +++ /dev/null @@ -1,494 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A single precision 3D vector. - /// - public struct Vector3 : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector3 zero = new Vector3(0, 0, 0); - #endregion - - #region Consts - /// - /// The vector epsilon. - /// - public const float Epsilon = 9.99999944E-11f; - #endregion - - #region Fields - /// - /// The x component. - /// - public float x; - /// - /// The y component. - /// - public float y; - /// - /// The z component. - /// - public float z; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public float Magnitude - { - get { return (float)System.Math.Sqrt(x * x + y * y + z * z); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public float MagnitudeSqr - { - get { return (x * x + y * y + z * z); } - } - - /// - /// Gets a normalized vector from this vector. - /// - public Vector3 Normalized - { - get - { - Vector3 result; - Normalize(ref this, out result); - return result; - } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public float this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - case 2: - return z; - default: - throw new IndexOutOfRangeException("Invalid Vector3 index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - case 2: - z = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector3 index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector3(float value) - { - this.x = value; - this.y = value; - this.z = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - /// The z value. - public Vector3(float x, float y, float z) - { - this.x = x; - this.y = y; - this.z = z; - } - - /// - /// Creates a new vector from a double precision vector. - /// - /// The double precision vector. - public Vector3(Vector3d vector) - { - this.x = (float)vector.x; - this.y = (float)vector.y; - this.z = (float)vector.z; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector3 operator +(Vector3 a, Vector3 b) - { - return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector3 operator -(Vector3 a, Vector3 b) - { - return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector3 operator *(Vector3 a, float d) - { - return new Vector3(a.x * d, a.y * d, a.z * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector3 operator *(float d, Vector3 a) - { - return new Vector3(a.x * d, a.y * d, a.z * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector3 operator /(Vector3 a, float d) - { - return new Vector3(a.x / d, a.y / d, a.z / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector3 operator -(Vector3 a) - { - return new Vector3(-a.x, -a.y, -a.z); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector3 lhs, Vector3 rhs) - { - return (lhs - rhs).MagnitudeSqr < Epsilon; - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector3 lhs, Vector3 rhs) - { - return (lhs - rhs).MagnitudeSqr >= Epsilon; - } - - /// - /// Explicitly converts from a double-precision vector into a single-precision vector. - /// - /// The double-precision vector. - public static explicit operator Vector3(Vector3d v) - { - return new Vector3((float)v.x, (float)v.y, (float)v.z); - } - - /// - /// Implicitly converts from an integer vector into a single-precision vector. - /// - /// The integer vector. - public static implicit operator Vector3(Vector3i v) - { - return new Vector3(v.x, v.y, v.z); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x, y and z components of an existing vector. - /// - /// The x value. - /// The y value. - /// The z value. - public void Set(float x, float y, float z) - { - this.x = x; - this.y = y; - this.z = z; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector3 scale) - { - x *= scale.x; - y *= scale.y; - z *= scale.z; - } - - /// - /// Normalizes this vector. - /// - public void Normalize() - { - float mag = this.Magnitude; - if (mag > Epsilon) - { - x /= mag; - y /= mag; - z /= mag; - } - else - { - x = y = z = 0; - } - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(float min, float max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - - if (z < min) z = min; - else if (z > max) z = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector3)) - { - return false; - } - Vector3 vector = (Vector3)other; - return (x == vector.x && y == vector.y && z == vector.z); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector3 other) - { - return (x == other.x && y == other.y && z == other.z); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1}, {2})", - x.ToString("F1", CultureInfo.InvariantCulture), - y.ToString("F1", CultureInfo.InvariantCulture), - z.ToString("F1", CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The float format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1}, {2})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture), - z.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Dot Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - public static float Dot(ref Vector3 lhs, ref Vector3 rhs) - { - return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; - } - - /// - /// Cross Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - /// The resulting vector. - public static void Cross(ref Vector3 lhs, ref Vector3 rhs, out Vector3 result) - { - result = new Vector3(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x); - } - - /// - /// Calculates the angle between two vectors. - /// - /// The from vector. - /// The to vector. - /// The angle. - public static float Angle(ref Vector3 from, ref Vector3 to) - { - Vector3 fromNormalized = from.Normalized; - Vector3 toNormalized = to.Normalized; - return (float)System.Math.Acos(MathHelper.Clamp(Vector3.Dot(ref fromNormalized, ref toNormalized), -1f, 1f)) * MathHelper.Rad2Deg; - } - - /// - /// Performs a linear interpolation between two vectors. - /// - /// The vector to interpolate from. - /// The vector to interpolate to. - /// The time fraction. - /// The resulting vector. - public static void Lerp(ref Vector3 a, ref Vector3 b, float t, out Vector3 result) - { - result = new Vector3(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t); - } - - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector3 a, ref Vector3 b, out Vector3 result) - { - result = new Vector3(a.x * b.x, a.y * b.y, a.z * b.z); - } - - /// - /// Normalizes a vector. - /// - /// The vector to normalize. - /// The resulting normalized vector. - public static void Normalize(ref Vector3 value, out Vector3 result) - { - float mag = value.Magnitude; - if (mag > Epsilon) - { - result = new Vector3(value.x / mag, value.y / mag, value.z / mag); - } - else - { - result = Vector3.zero; - } - } - - /// - /// Normalizes both vectors and makes them orthogonal to each other. - /// - /// The normal vector. - /// The tangent. - public static void OrthoNormalize(ref Vector3 normal, ref Vector3 tangent) - { - normal.Normalize(); - Vector3 proj = normal * Vector3.Dot(ref tangent, ref normal); - tangent -= proj; - tangent.Normalize(); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs deleted file mode 100644 index 11ebed1..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs +++ /dev/null @@ -1,481 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A double precision 3D vector. - /// - public struct Vector3d : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector3d zero = new Vector3d(0, 0, 0); - #endregion - - #region Consts - /// - /// The vector epsilon. - /// - public const double Epsilon = double.Epsilon; - #endregion - - #region Fields - /// - /// The x component. - /// - public double x; - /// - /// The y component. - /// - public double y; - /// - /// The z component. - /// - public double z; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public double Magnitude - { - get { return System.Math.Sqrt(x * x + y * y + z * z); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public double MagnitudeSqr - { - get { return (x * x + y * y + z * z); } - } - - /// - /// Gets a normalized vector from this vector. - /// - public Vector3d Normalized - { - get - { - Vector3d result; - Normalize(ref this, out result); - return result; - } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public double this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - case 2: - return z; - default: - throw new IndexOutOfRangeException("Invalid Vector3d index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - case 2: - z = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector3d index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector3d(double value) - { - this.x = value; - this.y = value; - this.z = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - /// The z value. - public Vector3d(double x, double y, double z) - { - this.x = x; - this.y = y; - this.z = z; - } - - /// - /// Creates a new vector from a single precision vector. - /// - /// The single precision vector. - public Vector3d(Vector3 vector) - { - this.x = vector.x; - this.y = vector.y; - this.z = vector.z; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector3d operator +(Vector3d a, Vector3d b) - { - return new Vector3d(a.x + b.x, a.y + b.y, a.z + b.z); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector3d operator -(Vector3d a, Vector3d b) - { - return new Vector3d(a.x - b.x, a.y - b.y, a.z - b.z); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector3d operator *(Vector3d a, double d) - { - return new Vector3d(a.x * d, a.y * d, a.z * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector3d operator *(double d, Vector3d a) - { - return new Vector3d(a.x * d, a.y * d, a.z * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector3d operator /(Vector3d a, double d) - { - return new Vector3d(a.x / d, a.y / d, a.z / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector3d operator -(Vector3d a) - { - return new Vector3d(-a.x, -a.y, -a.z); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector3d lhs, Vector3d rhs) - { - return (lhs - rhs).MagnitudeSqr < Epsilon; - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector3d lhs, Vector3d rhs) - { - return (lhs - rhs).MagnitudeSqr >= Epsilon; - } - - /// - /// Implicitly converts from a single-precision vector into a double-precision vector. - /// - /// The single-precision vector. - public static implicit operator Vector3d(Vector3 v) - { - return new Vector3d(v.x, v.y, v.z); - } - - /// - /// Implicitly converts from an integer vector into a double-precision vector. - /// - /// The integer vector. - public static implicit operator Vector3d(Vector3i v) - { - return new Vector3d(v.x, v.y, v.z); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x, y and z components of an existing vector. - /// - /// The x value. - /// The y value. - /// The z value. - public void Set(double x, double y, double z) - { - this.x = x; - this.y = y; - this.z = z; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector3d scale) - { - x *= scale.x; - y *= scale.y; - z *= scale.z; - } - - /// - /// Normalizes this vector. - /// - public void Normalize() - { - double mag = this.Magnitude; - if (mag > Epsilon) - { - x /= mag; - y /= mag; - z /= mag; - } - else - { - x = y = z = 0; - } - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(double min, double max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - - if (z < min) z = min; - else if (z > max) z = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector3d)) - { - return false; - } - Vector3d vector = (Vector3d)other; - return (x == vector.x && y == vector.y && z == vector.z); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector3d other) - { - return (x == other.x && y == other.y && z == other.z); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1}, {2})", - x.ToString("F1", CultureInfo.InvariantCulture), - y.ToString("F1", CultureInfo.InvariantCulture), - z.ToString("F1", CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The float format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1}, {2})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture), - z.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Dot Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - public static double Dot(ref Vector3d lhs, ref Vector3d rhs) - { - return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; - } - - /// - /// Cross Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - /// The resulting vector. - public static void Cross(ref Vector3d lhs, ref Vector3d rhs, out Vector3d result) - { - result = new Vector3d(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x); - } - - /// - /// Calculates the angle between two vectors. - /// - /// The from vector. - /// The to vector. - /// The angle. - public static double Angle(ref Vector3d from, ref Vector3d to) - { - Vector3d fromNormalized = from.Normalized; - Vector3d toNormalized = to.Normalized; - return System.Math.Acos(MathHelper.Clamp(Vector3d.Dot(ref fromNormalized, ref toNormalized), -1.0, 1.0)) * MathHelper.Rad2Degd; - } - - /// - /// Performs a linear interpolation between two vectors. - /// - /// The vector to interpolate from. - /// The vector to interpolate to. - /// The time fraction. - /// The resulting vector. - public static void Lerp(ref Vector3d a, ref Vector3d b, double t, out Vector3d result) - { - result = new Vector3d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t); - } - - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector3d a, ref Vector3d b, out Vector3d result) - { - result = new Vector3d(a.x * b.x, a.y * b.y, a.z * b.z); - } - - /// - /// Normalizes a vector. - /// - /// The vector to normalize. - /// The resulting normalized vector. - public static void Normalize(ref Vector3d value, out Vector3d result) - { - double mag = value.Magnitude; - if (mag > Epsilon) - { - result = new Vector3d(value.x / mag, value.y / mag, value.z / mag); - } - else - { - result = Vector3d.zero; - } - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs deleted file mode 100644 index d36d6d1..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs +++ /dev/null @@ -1,368 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A 3D integer vector. - /// - public struct Vector3i : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector3i zero = new Vector3i(0, 0, 0); - #endregion - - #region Fields - /// - /// The x component. - /// - public int x; - /// - /// The y component. - /// - public int y; - /// - /// The z component. - /// - public int z; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public int Magnitude - { - get { return (int)System.Math.Sqrt(x * x + y * y + z * z); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public int MagnitudeSqr - { - get { return (x * x + y * y + z * z); } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public int this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - case 2: - return z; - default: - throw new IndexOutOfRangeException("Invalid Vector3i index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - case 2: - z = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector3i index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector3i(int value) - { - this.x = value; - this.y = value; - this.z = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - /// The z value. - public Vector3i(int x, int y, int z) - { - this.x = x; - this.y = y; - this.z = z; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector3i operator +(Vector3i a, Vector3i b) - { - return new Vector3i(a.x + b.x, a.y + b.y, a.z + b.z); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector3i operator -(Vector3i a, Vector3i b) - { - return new Vector3i(a.x - b.x, a.y - b.y, a.z - b.z); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector3i operator *(Vector3i a, int d) - { - return new Vector3i(a.x * d, a.y * d, a.z * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector3i operator *(int d, Vector3i a) - { - return new Vector3i(a.x * d, a.y * d, a.z * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector3i operator /(Vector3i a, int d) - { - return new Vector3i(a.x / d, a.y / d, a.z / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector3i operator -(Vector3i a) - { - return new Vector3i(-a.x, -a.y, -a.z); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector3i lhs, Vector3i rhs) - { - return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z); - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector3i lhs, Vector3i rhs) - { - return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z); - } - - /// - /// Explicitly converts from a single-precision vector into an integer vector. - /// - /// The single-precision vector. - public static implicit operator Vector3i(Vector3 v) - { - return new Vector3i((int)v.x, (int)v.y, (int)v.z); - } - - /// - /// Explicitly converts from a double-precision vector into an integer vector. - /// - /// The double-precision vector. - public static explicit operator Vector3i(Vector3d v) - { - return new Vector3i((int)v.x, (int)v.y, (int)v.z); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x, y and z components of an existing vector. - /// - /// The x value. - /// The y value. - /// The z value. - public void Set(int x, int y, int z) - { - this.x = x; - this.y = y; - this.z = z; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector3i scale) - { - x *= scale.x; - y *= scale.y; - z *= scale.z; - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(int min, int max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - - if (z < min) z = min; - else if (z > max) z = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector3i)) - { - return false; - } - Vector3i vector = (Vector3i)other; - return (x == vector.x && y == vector.y && z == vector.z); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector3i other) - { - return (x == other.x && y == other.y && z == other.z); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1}, {2})", - x.ToString(CultureInfo.InvariantCulture), - y.ToString(CultureInfo.InvariantCulture), - z.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The integer format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1}, {2})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture), - z.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector3i a, ref Vector3i b, out Vector3i result) - { - result = new Vector3i(a.x * b.x, a.y * b.y, a.z * b.z); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs deleted file mode 100644 index bf1d655..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs +++ /dev/null @@ -1,467 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A single precision 4D vector. - /// - public struct Vector4 : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector4 zero = new Vector4(0, 0, 0, 0); - #endregion - - #region Consts - /// - /// The vector epsilon. - /// - public const float Epsilon = 9.99999944E-11f; - #endregion - - #region Fields - /// - /// The x component. - /// - public float x; - /// - /// The y component. - /// - public float y; - /// - /// The z component. - /// - public float z; - /// - /// The w component. - /// - public float w; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public float Magnitude - { - get { return (float)System.Math.Sqrt(x * x + y * y + z * z + w * w); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public float MagnitudeSqr - { - get { return (x * x + y * y + z * z + w * w); } - } - - /// - /// Gets a normalized vector from this vector. - /// - public Vector4 Normalized - { - get - { - Vector4 result; - Normalize(ref this, out result); - return result; - } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public float this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - case 2: - return z; - case 3: - return w; - default: - throw new IndexOutOfRangeException("Invalid Vector4 index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - case 2: - z = value; - break; - case 3: - w = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector4 index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector4(float value) - { - this.x = value; - this.y = value; - this.z = value; - this.w = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - /// The z value. - /// The w value. - public Vector4(float x, float y, float z, float w) - { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector4 operator +(Vector4 a, Vector4 b) - { - return new Vector4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector4 operator -(Vector4 a, Vector4 b) - { - return new Vector4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector4 operator *(Vector4 a, float d) - { - return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector4 operator *(float d, Vector4 a) - { - return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector4 operator /(Vector4 a, float d) - { - return new Vector4(a.x / d, a.y / d, a.z / d, a.w / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector4 operator -(Vector4 a) - { - return new Vector4(-a.x, -a.y, -a.z, -a.w); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector4 lhs, Vector4 rhs) - { - return (lhs - rhs).MagnitudeSqr < Epsilon; - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector4 lhs, Vector4 rhs) - { - return (lhs - rhs).MagnitudeSqr >= Epsilon; - } - - /// - /// Explicitly converts from a double-precision vector into a single-precision vector. - /// - /// The double-precision vector. - public static explicit operator Vector4(Vector4d v) - { - return new Vector4((float)v.x, (float)v.y, (float)v.z, (float)v.w); - } - - /// - /// Implicitly converts from an integer vector into a single-precision vector. - /// - /// The integer vector. - public static implicit operator Vector4(Vector4i v) - { - return new Vector4(v.x, v.y, v.z, v.w); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x, y and z components of an existing vector. - /// - /// The x value. - /// The y value. - /// The z value. - /// The w value. - public void Set(float x, float y, float z, float w) - { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector4 scale) - { - x *= scale.x; - y *= scale.y; - z *= scale.z; - w *= scale.w; - } - - /// - /// Normalizes this vector. - /// - public void Normalize() - { - float mag = this.Magnitude; - if (mag > Epsilon) - { - x /= mag; - y /= mag; - z /= mag; - w /= mag; - } - else - { - x = y = z = w = 0; - } - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(float min, float max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - - if (z < min) z = min; - else if (z > max) z = max; - - if (w < min) w = min; - else if (w > max) w = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector4)) - { - return false; - } - Vector4 vector = (Vector4)other; - return (x == vector.x && y == vector.y && z == vector.z && w == vector.w); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector4 other) - { - return (x == other.x && y == other.y && z == other.z && w == other.w); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1}, {2}, {3})", - x.ToString("F1", CultureInfo.InvariantCulture), - y.ToString("F1", CultureInfo.InvariantCulture), - z.ToString("F1", CultureInfo.InvariantCulture), - w.ToString("F1", CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The float format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1}, {2}, {3})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture), - z.ToString(format, CultureInfo.InvariantCulture), - w.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Dot Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - public static float Dot(ref Vector4 lhs, ref Vector4 rhs) - { - return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w; - } - - /// - /// Performs a linear interpolation between two vectors. - /// - /// The vector to interpolate from. - /// The vector to interpolate to. - /// The time fraction. - /// The resulting vector. - public static void Lerp(ref Vector4 a, ref Vector4 b, float t, out Vector4 result) - { - result = new Vector4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); - } - - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector4 a, ref Vector4 b, out Vector4 result) - { - result = new Vector4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); - } - - /// - /// Normalizes a vector. - /// - /// The vector to normalize. - /// The resulting normalized vector. - public static void Normalize(ref Vector4 value, out Vector4 result) - { - float mag = value.Magnitude; - if (mag > Epsilon) - { - result = new Vector4(value.x / mag, value.y / mag, value.z / mag, value.w / mag); - } - else - { - result = Vector4.zero; - } - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs deleted file mode 100644 index c984c08..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs +++ /dev/null @@ -1,467 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A double precision 4D vector. - /// - public struct Vector4d : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector4d zero = new Vector4d(0, 0, 0, 0); - #endregion - - #region Consts - /// - /// The vector epsilon. - /// - public const double Epsilon = double.Epsilon; - #endregion - - #region Fields - /// - /// The x component. - /// - public double x; - /// - /// The y component. - /// - public double y; - /// - /// The z component. - /// - public double z; - /// - /// The w component. - /// - public double w; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public double Magnitude - { - get { return System.Math.Sqrt(x * x + y * y + z * z + w * w); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public double MagnitudeSqr - { - get { return (x * x + y * y + z * z + w * w); } - } - - /// - /// Gets a normalized vector from this vector. - /// - public Vector4d Normalized - { - get - { - Vector4d result; - Normalize(ref this, out result); - return result; - } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public double this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - case 2: - return z; - case 3: - return w; - default: - throw new IndexOutOfRangeException("Invalid Vector4d index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - case 2: - z = value; - break; - case 3: - w = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector4d index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector4d(double value) - { - this.x = value; - this.y = value; - this.z = value; - this.w = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - /// The z value. - /// The w value. - public Vector4d(double x, double y, double z, double w) - { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector4d operator +(Vector4d a, Vector4d b) - { - return new Vector4d(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector4d operator -(Vector4d a, Vector4d b) - { - return new Vector4d(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector4d operator *(Vector4d a, double d) - { - return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector4d operator *(double d, Vector4d a) - { - return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector4d operator /(Vector4d a, double d) - { - return new Vector4d(a.x / d, a.y / d, a.z / d, a.w / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector4d operator -(Vector4d a) - { - return new Vector4d(-a.x, -a.y, -a.z, -a.w); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector4d lhs, Vector4d rhs) - { - return (lhs - rhs).MagnitudeSqr < Epsilon; - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector4d lhs, Vector4d rhs) - { - return (lhs - rhs).MagnitudeSqr >= Epsilon; - } - - /// - /// Implicitly converts from a single-precision vector into a double-precision vector. - /// - /// The single-precision vector. - public static implicit operator Vector4d(Vector4 v) - { - return new Vector4d(v.x, v.y, v.z, v.w); - } - - /// - /// Implicitly converts from an integer vector into a double-precision vector. - /// - /// The integer vector. - public static implicit operator Vector4d(Vector4i v) - { - return new Vector4d(v.x, v.y, v.z, v.w); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x, y and z components of an existing vector. - /// - /// The x value. - /// The y value. - /// The z value. - /// The w value. - public void Set(double x, double y, double z, double w) - { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector4d scale) - { - x *= scale.x; - y *= scale.y; - z *= scale.z; - w *= scale.w; - } - - /// - /// Normalizes this vector. - /// - public void Normalize() - { - double mag = this.Magnitude; - if (mag > Epsilon) - { - x /= mag; - y /= mag; - z /= mag; - w /= mag; - } - else - { - x = y = z = w = 0; - } - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(double min, double max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - - if (z < min) z = min; - else if (z > max) z = max; - - if (w < min) w = min; - else if (w > max) w = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector4d)) - { - return false; - } - Vector4d vector = (Vector4d)other; - return (x == vector.x && y == vector.y && z == vector.z && w == vector.w); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector4d other) - { - return (x == other.x && y == other.y && z == other.z && w == other.w); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1}, {2}, {3})", - x.ToString("F1", CultureInfo.InvariantCulture), - y.ToString("F1", CultureInfo.InvariantCulture), - z.ToString("F1", CultureInfo.InvariantCulture), - w.ToString("F1", CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The float format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1}, {2}, {3})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture), - z.ToString(format, CultureInfo.InvariantCulture), - w.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Dot Product of two vectors. - /// - /// The left hand side vector. - /// The right hand side vector. - public static double Dot(ref Vector4d lhs, ref Vector4d rhs) - { - return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w; - } - - /// - /// Performs a linear interpolation between two vectors. - /// - /// The vector to interpolate from. - /// The vector to interpolate to. - /// The time fraction. - /// The resulting vector. - public static void Lerp(ref Vector4d a, ref Vector4d b, double t, out Vector4d result) - { - result = new Vector4d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); - } - - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector4d a, ref Vector4d b, out Vector4d result) - { - result = new Vector4d(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); - } - - /// - /// Normalizes a vector. - /// - /// The vector to normalize. - /// The resulting normalized vector. - public static void Normalize(ref Vector4d value, out Vector4d result) - { - double mag = value.Magnitude; - if (mag > Epsilon) - { - result = new Vector4d(value.x / mag, value.y / mag, value.z / mag, value.w / mag); - } - else - { - result = Vector4d.zero; - } - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs b/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs deleted file mode 100644 index cc52459..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs +++ /dev/null @@ -1,388 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Globalization; - -namespace MeshDecimator.Math -{ - /// - /// A 4D integer vector. - /// - public struct Vector4i : IEquatable - { - #region Static Read-Only - /// - /// The zero vector. - /// - public static readonly Vector4i zero = new Vector4i(0, 0, 0, 0); - #endregion - - #region Fields - /// - /// The x component. - /// - public int x; - /// - /// The y component. - /// - public int y; - /// - /// The z component. - /// - public int z; - /// - /// The w component. - /// - public int w; - #endregion - - #region Properties - /// - /// Gets the magnitude of this vector. - /// - public int Magnitude - { - get { return (int)System.Math.Sqrt(x * x + y * y + z * z + w * w); } - } - - /// - /// Gets the squared magnitude of this vector. - /// - public int MagnitudeSqr - { - get { return (x * x + y * y + z * z + w * w); } - } - - /// - /// Gets or sets a specific component by index in this vector. - /// - /// The component index. - public int this[int index] - { - get - { - switch (index) - { - case 0: - return x; - case 1: - return y; - case 2: - return z; - case 3: - return w; - default: - throw new IndexOutOfRangeException("Invalid Vector4i index!"); - } - } - set - { - switch (index) - { - case 0: - x = value; - break; - case 1: - y = value; - break; - case 2: - z = value; - break; - case 3: - w = value; - break; - default: - throw new IndexOutOfRangeException("Invalid Vector4i index!"); - } - } - } - #endregion - - #region Constructor - /// - /// Creates a new vector with one value for all components. - /// - /// The value. - public Vector4i(int value) - { - this.x = value; - this.y = value; - this.z = value; - this.w = value; - } - - /// - /// Creates a new vector. - /// - /// The x value. - /// The y value. - /// The z value. - /// The w value. - public Vector4i(int x, int y, int z, int w) - { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - #endregion - - #region Operators - /// - /// Adds two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector4i operator +(Vector4i a, Vector4i b) - { - return new Vector4i(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); - } - - /// - /// Subtracts two vectors. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static Vector4i operator -(Vector4i a, Vector4i b) - { - return new Vector4i(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); - } - - /// - /// Scales the vector uniformly. - /// - /// The vector. - /// The scaling value. - /// The resulting vector. - public static Vector4i operator *(Vector4i a, int d) - { - return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d); - } - - /// - /// Scales the vector uniformly. - /// - /// The scaling value. - /// The vector. - /// The resulting vector. - public static Vector4i operator *(int d, Vector4i a) - { - return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d); - } - - /// - /// Divides the vector with a float. - /// - /// The vector. - /// The dividing float value. - /// The resulting vector. - public static Vector4i operator /(Vector4i a, int d) - { - return new Vector4i(a.x / d, a.y / d, a.z / d, a.w / d); - } - - /// - /// Subtracts the vector from a zero vector. - /// - /// The vector. - /// The resulting vector. - public static Vector4i operator -(Vector4i a) - { - return new Vector4i(-a.x, -a.y, -a.z, -a.w); - } - - /// - /// Returns if two vectors equals eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If equals. - public static bool operator ==(Vector4i lhs, Vector4i rhs) - { - return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w); - } - - /// - /// Returns if two vectors don't equal eachother. - /// - /// The left hand side vector. - /// The right hand side vector. - /// If not equals. - public static bool operator !=(Vector4i lhs, Vector4i rhs) - { - return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w); - } - - /// - /// Explicitly converts from a single-precision vector into an integer vector. - /// - /// The single-precision vector. - public static explicit operator Vector4i(Vector4 v) - { - return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w); - } - - /// - /// Explicitly converts from a double-precision vector into an integer vector. - /// - /// The double-precision vector. - public static explicit operator Vector4i(Vector4d v) - { - return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w); - } - #endregion - - #region Public Methods - #region Instance - /// - /// Set x, y and z components of an existing vector. - /// - /// The x value. - /// The y value. - /// The z value. - /// The w value. - public void Set(int x, int y, int z, int w) - { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - /// - /// Multiplies with another vector component-wise. - /// - /// The vector to multiply with. - public void Scale(ref Vector4i scale) - { - x *= scale.x; - y *= scale.y; - z *= scale.z; - w *= scale.w; - } - - /// - /// Clamps this vector between a specific range. - /// - /// The minimum component value. - /// The maximum component value. - public void Clamp(int min, int max) - { - if (x < min) x = min; - else if (x > max) x = max; - - if (y < min) y = min; - else if (y > max) y = max; - - if (z < min) z = min; - else if (z > max) z = max; - - if (w < min) w = min; - else if (w > max) w = max; - } - #endregion - - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1; - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public override bool Equals(object other) - { - if (!(other is Vector4i)) - { - return false; - } - Vector4i vector = (Vector4i)other; - return (x == vector.x && y == vector.y && z == vector.z && w == vector.w); - } - - /// - /// Returns if this vector is equal to another one. - /// - /// The other vector to compare to. - /// If equals. - public bool Equals(Vector4i other) - { - return (x == other.x && y == other.y && z == other.z && w == other.w); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}, {1}, {2}, {3})", - x.ToString(CultureInfo.InvariantCulture), - y.ToString(CultureInfo.InvariantCulture), - z.ToString(CultureInfo.InvariantCulture), - w.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Returns a nicely formatted string for this vector. - /// - /// The integer format. - /// The string. - public string ToString(string format) - { - return string.Format("({0}, {1}, {2}, {3})", - x.ToString(format, CultureInfo.InvariantCulture), - y.ToString(format, CultureInfo.InvariantCulture), - z.ToString(format, CultureInfo.InvariantCulture), - w.ToString(format, CultureInfo.InvariantCulture)); - } - #endregion - - #region Static - /// - /// Multiplies two vectors component-wise. - /// - /// The first vector. - /// The second vector. - /// The resulting vector. - public static void Scale(ref Vector4i a, ref Vector4i b, out Vector4i result) - { - result = new Vector4i(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/Mesh.cs b/LightlessSync/ThirdParty/MeshDecimator/Mesh.cs deleted file mode 100644 index 416ad4e..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/Mesh.cs +++ /dev/null @@ -1,1006 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using System.Collections.Generic; -using MeshDecimator.Math; - -namespace MeshDecimator -{ - /// - /// A mesh. - /// - public sealed class Mesh - { - #region Consts - /// - /// The count of supported UV channels. - /// - public const int UVChannelCount = 4; - #endregion - - #region Fields - private Vector3d[] vertices = null; - private int[][] indices = null; - private Vector3[] normals = null; - private Vector4[] tangents = null; - private Vector4[] tangents2 = null; - private Vector2[][] uvs2D = null; - private Vector3[][] uvs3D = null; - private Vector4[][] uvs4D = null; - private Vector4[] colors = null; - private BoneWeight[] boneWeights = null; - private float[] positionWs = null; - private float[] normalWs = null; - - private static readonly int[] emptyIndices = new int[0]; - #endregion - - #region Properties - /// - /// Gets the count of vertices of this mesh. - /// - public int VertexCount - { - get { return vertices.Length; } - } - - /// - /// Gets or sets the count of submeshes in this mesh. - /// - public int SubMeshCount - { - get { return indices.Length; } - set - { - if (value <= 0) - throw new ArgumentOutOfRangeException("value"); - - int[][] newIndices = new int[value][]; - Array.Copy(indices, 0, newIndices, 0, MathHelper.Min(indices.Length, newIndices.Length)); - indices = newIndices; - } - } - - /// - /// Gets the total count of triangles in this mesh. - /// - public int TriangleCount - { - get - { - int triangleCount = 0; - for (int i = 0; i < indices.Length; i++) - { - if (indices[i] != null) - { - triangleCount += indices[i].Length / 3; - } - } - return triangleCount; - } - } - - /// - /// Gets or sets the vertices for this mesh. Note that this resets all other vertex attributes. - /// - public Vector3d[] Vertices - { - get { return vertices; } - set - { - if (value == null) - throw new ArgumentNullException("value"); - - vertices = value; - ClearVertexAttributes(); - } - } - - /// - /// Gets or sets the combined indices for this mesh. Once set, the sub-mesh count gets set to 1. - /// - public int[] Indices - { - get - { - if (indices.Length == 1) - { - return indices[0] ?? emptyIndices; - } - else - { - List indexList = new List(TriangleCount * 3); - for (int i = 0; i < indices.Length; i++) - { - if (indices[i] != null) - { - indexList.AddRange(indices[i]); - } - } - return indexList.ToArray(); - } - } - set - { - if (value == null) - throw new ArgumentNullException("value"); - else if ((value.Length % 3) != 0) - throw new ArgumentException("The index count must be multiple by 3.", "value"); - - SubMeshCount = 1; - SetIndices(0, value); - } - } - - /// - /// Gets or sets the normals for this mesh. - /// - public Vector3[] Normals - { - get { return normals; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The vertex normals must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - normals = value; - } - } - - /// - /// Gets or sets the position W components for this mesh. - /// - public float[] PositionWs - { - get { return positionWs; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The position Ws must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - positionWs = value; - } - } - - /// - /// Gets or sets the normal W components for this mesh. - /// - public float[] NormalWs - { - get { return normalWs; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The normal Ws must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - normalWs = value; - } - } - - /// - /// Gets or sets the tangents for this mesh. - /// - public Vector4[] Tangents - { - get { return tangents; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The vertex tangents must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - tangents = value; - } - } - - /// - /// Gets or sets the second tangent set for this mesh. - /// - public Vector4[] Tangents2 - { - get { return tangents2; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The second vertex tangents must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - tangents2 = value; - } - } - - /// - /// Gets or sets the first UV set for this mesh. - /// - public Vector2[] UV1 - { - get { return GetUVs2D(0); } - set { SetUVs(0, value); } - } - - /// - /// Gets or sets the second UV set for this mesh. - /// - public Vector2[] UV2 - { - get { return GetUVs2D(1); } - set { SetUVs(1, value); } - } - - /// - /// Gets or sets the third UV set for this mesh. - /// - public Vector2[] UV3 - { - get { return GetUVs2D(2); } - set { SetUVs(2, value); } - } - - /// - /// Gets or sets the fourth UV set for this mesh. - /// - public Vector2[] UV4 - { - get { return GetUVs2D(3); } - set { SetUVs(3, value); } - } - - /// - /// Gets or sets the vertex colors for this mesh. - /// - public Vector4[] Colors - { - get { return colors; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The vertex colors must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - colors = value; - } - } - - /// - /// Gets or sets the vertex bone weights for this mesh. - /// - public BoneWeight[] BoneWeights - { - get { return boneWeights; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The vertex bone weights must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - boneWeights = value; - } - } - #endregion - - #region Constructor - /// - /// Creates a new mesh. - /// - /// The mesh vertices. - /// The mesh indices. - public Mesh(Vector3d[] vertices, int[] indices) - { - if (vertices == null) - throw new ArgumentNullException("vertices"); - else if (indices == null) - throw new ArgumentNullException("indices"); - else if ((indices.Length % 3) != 0) - throw new ArgumentException("The index count must be multiple by 3.", "indices"); - - this.vertices = vertices; - this.indices = new int[1][]; - this.indices[0] = indices; - } - - /// - /// Creates a new mesh. - /// - /// The mesh vertices. - /// The mesh indices. - public Mesh(Vector3d[] vertices, int[][] indices) - { - if (vertices == null) - throw new ArgumentNullException("vertices"); - else if (indices == null) - throw new ArgumentNullException("indices"); - - for (int i = 0; i < indices.Length; i++) - { - if (indices[i] != null && (indices[i].Length % 3) != 0) - throw new ArgumentException(string.Format("The index count must be multiple by 3 at sub-mesh index {0}.", i), "indices"); - } - - this.vertices = vertices; - this.indices = indices; - } - #endregion - - #region Private Methods - private void ClearVertexAttributes() - { - normals = null; - tangents = null; - tangents2 = null; - uvs2D = null; - uvs3D = null; - uvs4D = null; - colors = null; - boneWeights = null; - positionWs = null; - normalWs = null; - } - #endregion - - #region Public Methods - #region Recalculate Normals - /// - /// Recalculates the normals for this mesh smoothly. - /// - public void RecalculateNormals() - { - int vertexCount = vertices.Length; - Vector3[] normals = new Vector3[vertexCount]; - - int subMeshCount = this.indices.Length; - for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) - { - int[] indices = this.indices[subMeshIndex]; - if (indices == null) - continue; - - int indexCount = indices.Length; - for (int i = 0; i < indexCount; i += 3) - { - int i0 = indices[i]; - int i1 = indices[i + 1]; - int i2 = indices[i + 2]; - - var v0 = (Vector3)vertices[i0]; - var v1 = (Vector3)vertices[i1]; - var v2 = (Vector3)vertices[i2]; - - var nx = v1 - v0; - var ny = v2 - v0; - Vector3 normal; - Vector3.Cross(ref nx, ref ny, out normal); - normal.Normalize(); - - normals[i0] += normal; - normals[i1] += normal; - normals[i2] += normal; - } - } - - for (int i = 0; i < vertexCount; i++) - { - normals[i].Normalize(); - } - - this.normals = normals; - } - #endregion - - #region Recalculate Tangents - /// - /// Recalculates the tangents for this mesh. - /// - public void RecalculateTangents() - { - // Make sure we have the normals first - if (normals == null) - return; - - // Also make sure that we have the first UV set - bool uvIs2D = (uvs2D != null && uvs2D[0] != null); - bool uvIs3D = (uvs3D != null && uvs3D[0] != null); - bool uvIs4D = (uvs4D != null && uvs4D[0] != null); - if (!uvIs2D && !uvIs3D && !uvIs4D) - return; - - int vertexCount = vertices.Length; - - var tangents = new Vector4[vertexCount]; - var tan1 = new Vector3[vertexCount]; - var tan2 = new Vector3[vertexCount]; - - Vector2[] uv2D = (uvIs2D ? uvs2D[0] : null); - Vector3[] uv3D = (uvIs3D ? uvs3D[0] : null); - Vector4[] uv4D = (uvIs4D ? uvs4D[0] : null); - - int subMeshCount = this.indices.Length; - for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) - { - int[] indices = this.indices[subMeshIndex]; - if (indices == null) - continue; - - int indexCount = indices.Length; - for (int i = 0; i < indexCount; i += 3) - { - int i0 = indices[i]; - int i1 = indices[i + 1]; - int i2 = indices[i + 2]; - - var v0 = vertices[i0]; - var v1 = vertices[i1]; - var v2 = vertices[i2]; - - float s1, s2, t1, t2; - if (uvIs2D) - { - var w0 = uv2D[i0]; - var w1 = uv2D[i1]; - var w2 = uv2D[i2]; - s1 = w1.x - w0.x; - s2 = w2.x - w0.x; - t1 = w1.y - w0.y; - t2 = w2.y - w0.y; - } - else if (uvIs3D) - { - var w0 = uv3D[i0]; - var w1 = uv3D[i1]; - var w2 = uv3D[i2]; - s1 = w1.x - w0.x; - s2 = w2.x - w0.x; - t1 = w1.y - w0.y; - t2 = w2.y - w0.y; - } - else - { - var w0 = uv4D[i0]; - var w1 = uv4D[i1]; - var w2 = uv4D[i2]; - s1 = w1.x - w0.x; - s2 = w2.x - w0.x; - t1 = w1.y - w0.y; - t2 = w2.y - w0.y; - } - - - float x1 = (float)(v1.x - v0.x); - float x2 = (float)(v2.x - v0.x); - float y1 = (float)(v1.y - v0.y); - float y2 = (float)(v2.y - v0.y); - float z1 = (float)(v1.z - v0.z); - float z2 = (float)(v2.z - v0.z); - float r = 1f / (s1 * t2 - s2 * t1); - - var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); - var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r); - - tan1[i0] += sdir; - tan1[i1] += sdir; - tan1[i2] += sdir; - tan2[i0] += tdir; - tan2[i1] += tdir; - tan2[i2] += tdir; - } - } - - for (int i = 0; i < vertexCount; i++) - { - var n = normals[i]; - var t = tan1[i]; - - var tmp = (t - n * Vector3.Dot(ref n, ref t)); - tmp.Normalize(); - - Vector3 c; - Vector3.Cross(ref n, ref t, out c); - float dot = Vector3.Dot(ref c, ref tan2[i]); - float w = (dot < 0f ? -1f : 1f); - tangents[i] = new Vector4(tmp.x, tmp.y, tmp.z, w); - } - - this.tangents = tangents; - } - #endregion - - #region Triangles - /// - /// Returns the count of triangles for a specific sub-mesh in this mesh. - /// - /// The sub-mesh index. - /// The triangle count. - public int GetTriangleCount(int subMeshIndex) - { - if (subMeshIndex < 0 || subMeshIndex >= indices.Length) - throw new IndexOutOfRangeException(); - - return indices[subMeshIndex].Length / 3; - } - - /// - /// Returns the triangle indices of a specific sub-mesh in this mesh. - /// - /// The sub-mesh index. - /// The triangle indices. - public int[] GetIndices(int subMeshIndex) - { - if (subMeshIndex < 0 || subMeshIndex >= indices.Length) - throw new IndexOutOfRangeException(); - - return indices[subMeshIndex] ?? emptyIndices; - } - - /// - /// Returns the triangle indices for all sub-meshes in this mesh. - /// - /// The sub-mesh triangle indices. - public int[][] GetSubMeshIndices() - { - var subMeshIndices = new int[indices.Length][]; - for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++) - { - subMeshIndices[subMeshIndex] = indices[subMeshIndex] ?? emptyIndices; - } - return subMeshIndices; - } - - /// - /// Sets the triangle indices of a specific sub-mesh in this mesh. - /// - /// The sub-mesh index. - /// The triangle indices. - public void SetIndices(int subMeshIndex, int[] indices) - { - if (subMeshIndex < 0 || subMeshIndex >= this.indices.Length) - throw new IndexOutOfRangeException(); - else if (indices == null) - throw new ArgumentNullException("indices"); - else if ((indices.Length % 3) != 0) - throw new ArgumentException("The index count must be multiple by 3.", "indices"); - - this.indices[subMeshIndex] = indices; - } - #endregion - - #region UV Sets - #region Getting - /// - /// Returns the UV dimension for a specific channel. - /// - /// - /// The UV dimension count. - public int GetUVDimension(int channel) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs2D != null && uvs2D[channel] != null) - { - return 2; - } - else if (uvs3D != null && uvs3D[channel] != null) - { - return 3; - } - else if (uvs4D != null && uvs4D[channel] != null) - { - return 4; - } - else - { - return 0; - } - } - - /// - /// Returns the UVs (2D) from a specific channel. - /// - /// The channel index. - /// The UVs. - public Vector2[] GetUVs2D(int channel) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs2D != null && uvs2D[channel] != null) - { - return uvs2D[channel]; - } - else - { - return null; - } - } - - /// - /// Returns the UVs (3D) from a specific channel. - /// - /// The channel index. - /// The UVs. - public Vector3[] GetUVs3D(int channel) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs3D != null && uvs3D[channel] != null) - { - return uvs3D[channel]; - } - else - { - return null; - } - } - - /// - /// Returns the UVs (4D) from a specific channel. - /// - /// The channel index. - /// The UVs. - public Vector4[] GetUVs4D(int channel) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs4D != null && uvs4D[channel] != null) - { - return uvs4D[channel]; - } - else - { - return null; - } - } - - /// - /// Returns the UVs (2D) from a specific channel. - /// - /// The channel index. - /// The UVs. - public void GetUVs(int channel, List uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - else if (uvs == null) - throw new ArgumentNullException("uvs"); - - uvs.Clear(); - if (uvs2D != null && uvs2D[channel] != null) - { - var uvData = uvs2D[channel]; - if (uvData != null) - { - uvs.AddRange(uvData); - } - } - } - - /// - /// Returns the UVs (3D) from a specific channel. - /// - /// The channel index. - /// The UVs. - public void GetUVs(int channel, List uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - else if (uvs == null) - throw new ArgumentNullException("uvs"); - - uvs.Clear(); - if (uvs3D != null && uvs3D[channel] != null) - { - var uvData = uvs3D[channel]; - if (uvData != null) - { - uvs.AddRange(uvData); - } - } - } - - /// - /// Returns the UVs (4D) from a specific channel. - /// - /// The channel index. - /// The UVs. - public void GetUVs(int channel, List uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - else if (uvs == null) - throw new ArgumentNullException("uvs"); - - uvs.Clear(); - if (uvs4D != null && uvs4D[channel] != null) - { - var uvData = uvs4D[channel]; - if (uvData != null) - { - uvs.AddRange(uvData); - } - } - } - #endregion - - #region Setting - /// - /// Sets the UVs (2D) for a specific channel. - /// - /// The channel index. - /// The UVs. - public void SetUVs(int channel, Vector2[] uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs != null && uvs.Length > 0) - { - if (uvs.Length != vertices.Length) - throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvs.Length, vertices.Length)); - - if (uvs2D == null) - uvs2D = new Vector2[UVChannelCount][]; - - int uvCount = uvs.Length; - var uvSet = new Vector2[uvCount]; - uvs2D[channel] = uvSet; - uvs.CopyTo(uvSet, 0); - } - else - { - if (uvs2D != null) - { - uvs2D[channel] = null; - } - } - - if (uvs3D != null) - { - uvs3D[channel] = null; - } - if (uvs4D != null) - { - uvs4D[channel] = null; - } - } - - /// - /// Sets the UVs (3D) for a specific channel. - /// - /// The channel index. - /// The UVs. - public void SetUVs(int channel, Vector3[] uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs != null && uvs.Length > 0) - { - int uvCount = uvs.Length; - if (uvCount != vertices.Length) - throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); - - if (uvs3D == null) - uvs3D = new Vector3[UVChannelCount][]; - - var uvSet = new Vector3[uvCount]; - uvs3D[channel] = uvSet; - uvs.CopyTo(uvSet, 0); - } - else - { - if (uvs3D != null) - { - uvs3D[channel] = null; - } - } - - if (uvs2D != null) - { - uvs2D[channel] = null; - } - if (uvs4D != null) - { - uvs4D[channel] = null; - } - } - - /// - /// Sets the UVs (4D) for a specific channel. - /// - /// The channel index. - /// The UVs. - public void SetUVs(int channel, Vector4[] uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs != null && uvs.Length > 0) - { - int uvCount = uvs.Length; - if (uvCount != vertices.Length) - throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); - - if (uvs4D == null) - uvs4D = new Vector4[UVChannelCount][]; - - var uvSet = new Vector4[uvCount]; - uvs4D[channel] = uvSet; - uvs.CopyTo(uvSet, 0); - } - else - { - if (uvs4D != null) - { - uvs4D[channel] = null; - } - } - - if (uvs2D != null) - { - uvs2D[channel] = null; - } - if (uvs3D != null) - { - uvs3D[channel] = null; - } - } - - /// - /// Sets the UVs (2D) for a specific channel. - /// - /// The channel index. - /// The UVs. - public void SetUVs(int channel, List uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs != null && uvs.Count > 0) - { - int uvCount = uvs.Count; - if (uvCount != vertices.Length) - throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); - - if (uvs2D == null) - uvs2D = new Vector2[UVChannelCount][]; - - var uvSet = new Vector2[uvCount]; - uvs2D[channel] = uvSet; - uvs.CopyTo(uvSet, 0); - } - else - { - if (uvs2D != null) - { - uvs2D[channel] = null; - } - } - - if (uvs3D != null) - { - uvs3D[channel] = null; - } - if (uvs4D != null) - { - uvs4D[channel] = null; - } - } - - /// - /// Sets the UVs (3D) for a specific channel. - /// - /// The channel index. - /// The UVs. - public void SetUVs(int channel, List uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs != null && uvs.Count > 0) - { - int uvCount = uvs.Count; - if (uvCount != vertices.Length) - throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); - - if (uvs3D == null) - uvs3D = new Vector3[UVChannelCount][]; - - var uvSet = new Vector3[uvCount]; - uvs3D[channel] = uvSet; - uvs.CopyTo(uvSet, 0); - } - else - { - if (uvs3D != null) - { - uvs3D[channel] = null; - } - } - - if (uvs2D != null) - { - uvs2D[channel] = null; - } - if (uvs4D != null) - { - uvs4D[channel] = null; - } - } - - /// - /// Sets the UVs (4D) for a specific channel. - /// - /// The channel index. - /// The UVs. - public void SetUVs(int channel, List uvs) - { - if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); - - if (uvs != null && uvs.Count > 0) - { - int uvCount = uvs.Count; - if (uvCount != vertices.Length) - throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs"); - - if (uvs4D == null) - uvs4D = new Vector4[UVChannelCount][]; - - var uvSet = new Vector4[uvCount]; - uvs4D[channel] = uvSet; - uvs.CopyTo(uvSet, 0); - } - else - { - if (uvs4D != null) - { - uvs4D[channel] = null; - } - } - - if (uvs2D != null) - { - uvs2D[channel] = null; - } - if (uvs3D != null) - { - uvs3D[channel] = null; - } - } - #endregion - #endregion - - #region To String - /// - /// Returns the text-representation of this mesh. - /// - /// The text-representation. - public override string ToString() - { - return string.Format("Vertices: {0}", vertices.Length); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs b/LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs deleted file mode 100644 index cb13fe8..0000000 --- a/LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs +++ /dev/null @@ -1,180 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 Mattias Edlund - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -#endregion - -using System; -using MeshDecimator.Algorithms; - -namespace MeshDecimator -{ - #region Algorithm - /// - /// The decimation algorithms. - /// - public enum Algorithm - { - /// - /// The default algorithm. - /// - Default, - /// - /// The fast quadric mesh simplification algorithm. - /// - FastQuadricMesh - } - #endregion - - /// - /// The mesh decimation API. - /// - public static class MeshDecimation - { - #region Public Methods - #region Create Algorithm - /// - /// Creates a specific decimation algorithm. - /// - /// The desired algorithm. - /// The decimation algorithm. - public static DecimationAlgorithm CreateAlgorithm(Algorithm algorithm) - { - DecimationAlgorithm alg = null; - - switch (algorithm) - { - case Algorithm.Default: - case Algorithm.FastQuadricMesh: - alg = new FastQuadricMeshSimplification(); - break; - default: - throw new ArgumentException("The specified algorithm is not supported.", "algorithm"); - } - - return alg; - } - #endregion - - #region Decimate Mesh - /// - /// Decimates a mesh. - /// - /// The mesh to decimate. - /// The target triangle count. - /// The decimated mesh. - public static Mesh DecimateMesh(Mesh mesh, int targetTriangleCount) - { - return DecimateMesh(Algorithm.Default, mesh, targetTriangleCount); - } - - /// - /// Decimates a mesh. - /// - /// The desired algorithm. - /// The mesh to decimate. - /// The target triangle count. - /// The decimated mesh. - public static Mesh DecimateMesh(Algorithm algorithm, Mesh mesh, int targetTriangleCount) - { - if (mesh == null) - throw new ArgumentNullException("mesh"); - - var decimationAlgorithm = CreateAlgorithm(algorithm); - return DecimateMesh(decimationAlgorithm, mesh, targetTriangleCount); - } - - /// - /// Decimates a mesh. - /// - /// The decimation algorithm. - /// The mesh to decimate. - /// The target triangle count. - /// The decimated mesh. - public static Mesh DecimateMesh(DecimationAlgorithm algorithm, Mesh mesh, int targetTriangleCount) - { - if (algorithm == null) - throw new ArgumentNullException("algorithm"); - else if (mesh == null) - throw new ArgumentNullException("mesh"); - - int currentTriangleCount = mesh.TriangleCount; - if (targetTriangleCount > currentTriangleCount) - targetTriangleCount = currentTriangleCount; - else if (targetTriangleCount < 0) - targetTriangleCount = 0; - - algorithm.Initialize(mesh); - algorithm.DecimateMesh(targetTriangleCount); - return algorithm.ToMesh(); - } - #endregion - - #region Decimate Mesh Lossless - /// - /// Decimates a mesh without losing any quality. - /// - /// The mesh to decimate. - /// The decimated mesh. - public static Mesh DecimateMeshLossless(Mesh mesh) - { - return DecimateMeshLossless(Algorithm.Default, mesh); - } - - /// - /// Decimates a mesh without losing any quality. - /// - /// The desired algorithm. - /// The mesh to decimate. - /// The decimated mesh. - public static Mesh DecimateMeshLossless(Algorithm algorithm, Mesh mesh) - { - if (mesh == null) - throw new ArgumentNullException("mesh"); - - var decimationAlgorithm = CreateAlgorithm(algorithm); - return DecimateMeshLossless(decimationAlgorithm, mesh); - } - - /// - /// Decimates a mesh without losing any quality. - /// - /// The decimation algorithm. - /// The mesh to decimate. - /// The decimated mesh. - public static Mesh DecimateMeshLossless(DecimationAlgorithm algorithm, Mesh mesh) - { - if (algorithm == null) - throw new ArgumentNullException("algorithm"); - else if (mesh == null) - throw new ArgumentNullException("mesh"); - - int currentTriangleCount = mesh.TriangleCount; - algorithm.Initialize(mesh); - algorithm.DecimateMeshLossless(); - return algorithm.ToMesh(); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/Decimate.cs b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/Decimate.cs new file mode 100644 index 0000000..a69485b --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/Decimate.cs @@ -0,0 +1,1325 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Nanomesh +{ + public partial class DecimateModifier + { + // Heuristics + internal static bool UpdateFarNeighbors = false; + internal static bool UpdateMinsOnCollapse = true; + internal static float MergeNormalsThresholdDegrees = 90; + internal static float NormalSimilarityThresholdDegrees = 60; + internal static float CollapseToMidpointPenalty = 0.4716252f; + internal static bool CollapseToEndpointsOnly = false; + internal static float UvSimilarityThreshold = 0.02f; + internal static float UvSeamAngleCos = 0.99f; + internal static bool BlockUvSeamVertices = true; + internal static float BoneWeightSimilarityThreshold = 0.85f; + internal static bool LimitCollapseEdgeLength = false; + internal static float MaxCollapseEdgeLength = float.PositiveInfinity; + internal static bool AllowBoundaryCollapses = false; + internal static float BodyCollisionPenetrationFactor = 0.75f; + + // Constants + private const double _DeterminantEpsilon = 0.001f; + private const float _MinTriangleAreaRatio = 0.05f; + private const float _UvDirEpsilonSq = 1e-12f; + private const double _OFFSET_HARD = 1e6; + private const double _OFFSET_NOCOLLAPSE = 1e300; + + // Instance + private ConnectedMesh _mesh; + private SymmetricMatrix[] _matrices; + private FastHashSet _pairs; + private LinkedHashSet _mins; + private int _lastProgress = int.MinValue; + private int _initialTriangleCount; + private float _mergeNormalsThresholdCos = MathF.Cos(MergeNormalsThresholdDegrees * MathF.PI / 180f); + private float _normalSimilarityThresholdCos = MathF.Cos(NormalSimilarityThresholdDegrees * MathF.PI / 180f); + private int _evaluatedEdges; + private int _collapsedEdges; + private int _rejectedBoneWeights; + private int _rejectedTopology; + private int _rejectedInversion; + private int _rejectedDegenerate; + private int _rejectedArea; + private int _rejectedFlip; + private int _rejectedBodyCollision; + private float[]? _bodyDistanceSq; + private float _bodyDistanceThresholdSq; + private Func? _bodyDistanceSqEvaluator; + private bool[]? _protectedVertices; + + public ConnectedMesh Mesh => _mesh; + + public DecimationStats GetStats() + => new DecimationStats( + _evaluatedEdges, + _collapsedEdges, + _rejectedBoneWeights, + _rejectedTopology, + _rejectedInversion, + _rejectedDegenerate, + _rejectedArea, + _rejectedFlip, + _rejectedBodyCollision); + + public void SetBodyCollision(float[]? bodyDistanceSq, float bodyDistanceThresholdSq, Func? bodyDistanceSqEvaluator = null) + { + _bodyDistanceSq = bodyDistanceSq; + _bodyDistanceThresholdSq = bodyDistanceThresholdSq; + _bodyDistanceSqEvaluator = bodyDistanceSqEvaluator; + } + + public void SetProtectedVertices(bool[]? protectedVertices) + { + _protectedVertices = protectedVertices; + } + + public void Initialize(ConnectedMesh mesh) + { + _mesh = mesh; + ResetStats(); + + _initialTriangleCount = mesh.FaceCount; + + _matrices = new SymmetricMatrix[mesh.positions.Length]; + _pairs = new FastHashSet(); + _mins = new LinkedHashSet(); + + InitializePairs(); + + for (int p = 0; p < _mesh.PositionToNode.Length; p++) + { + if (_mesh.PositionToNode[p] != -1) + CalculateQuadric(p); + } + + foreach (EdgeCollapse pair in _pairs) + { + CalculateError(pair); + } + } + + public void DecimateToError(float maximumError) + { + while (GetPairWithMinimumError().error <= maximumError && _pairs.Count > 0) + { + Iterate(); + } + } + + public void DecimateToRatio(float targetTriangleRatio) + { + targetTriangleRatio = MathF.Clamp(targetTriangleRatio, 0f, 1f); + DecimateToPolycount((int)MathF.Round(targetTriangleRatio * _mesh.FaceCount)); + } + + public void DecimatePolycount(int polycount) + { + DecimateToPolycount((int)MathF.Round(_mesh.FaceCount - polycount)); + } + + public void DecimateToPolycount(int targetTriangleCount) + { + while (_mesh.FaceCount > targetTriangleCount && _pairs.Count > 0) + { + Iterate(); + + int progress = (int)MathF.Round(100f * (_initialTriangleCount - _mesh.FaceCount) / (_initialTriangleCount - targetTriangleCount)); + if (progress >= _lastProgress + 10) + { + _lastProgress = progress; + } + } + } + + public void Iterate() + { + EdgeCollapse pair = GetPairWithMinimumError(); + while (pair != null && pair.error >= _OFFSET_NOCOLLAPSE) + { + _pairs.Remove(pair); + _mins.Remove(pair); + pair = GetPairWithMinimumError(); + } + + if (pair == null) + return; + + Debug.Assert(_mesh.CheckEdge(_mesh.PositionToNode[pair.posA], _mesh.PositionToNode[pair.posB])); + + _pairs.Remove(pair); + _mins.Remove(pair); + + CollapseEdge(pair); + } + + public double GetMinimumError() + { + return GetPairWithMinimumError()?.error ?? double.PositiveInfinity; + } + + private EdgeCollapse GetPairWithMinimumError() + { + if (_mins.Count == 0) + ComputeMins(); + + LinkedHashSet.LinkedHashNode edge = _mins.First; + + return edge?.Value; + } + + private int MinsCount => MathF.Clamp(500, 0, _pairs.Count); + + private void ComputeMins() + { + _mins = new LinkedHashSet(_pairs.OrderBy(x => x).Take(MinsCount)); + } + + private void InitializePairs() + { + _pairs.Clear(); + _mins.Clear(); + + for (int p = 0; p < _mesh.PositionToNode.Length; p++) + { + int nodeIndex = _mesh.PositionToNode[p]; + if (nodeIndex < 0) + { + continue; + } + + int sibling = nodeIndex; + do + { + int firstRelative = _mesh.nodes[sibling].relative; + int secondRelative = _mesh.nodes[firstRelative].relative; + + EdgeCollapse pair = new EdgeCollapse(_mesh.nodes[firstRelative].position, _mesh.nodes[secondRelative].position); + + _pairs.Add(pair); + + Debug.Assert(_mesh.CheckEdge(_mesh.PositionToNode[pair.posA], _mesh.PositionToNode[pair.posB])); + + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + } + } + + private void CalculateQuadric(int position) + { + int nodeIndex = _mesh.PositionToNode[position]; + + Debug.Assert(nodeIndex >= 0); + Debug.Assert(!_mesh.nodes[nodeIndex].IsRemoved); + + SymmetricMatrix symmetricMatrix = new SymmetricMatrix(); + + int sibling = nodeIndex; + do + { + Debug.Assert(_mesh.CheckRelatives(sibling)); + + Vector3 faceNormal = _mesh.GetFaceNormal(sibling); + double dot = Vector3.Dot(-faceNormal, _mesh.positions[_mesh.nodes[sibling].position]); + symmetricMatrix += new SymmetricMatrix(faceNormal.x, faceNormal.y, faceNormal.z, dot); + + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + + _matrices[position] = symmetricMatrix; + } + + private readonly HashSet _adjacentEdges = new HashSet(); + private readonly HashSet _adjacentEdgesA = new HashSet(); + private readonly HashSet _adjacentEdgesB = new HashSet(); + + private IEnumerable GetAdjacentPositions(int nodeIndex, int nodeAvoid) + { + _adjacentEdges.Clear(); + + int posToAvoid = _mesh.nodes[nodeAvoid].position; + + int sibling = nodeIndex; + do + { + for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;) + { + if (_mesh.nodes[relative].position != posToAvoid) + { + _adjacentEdges.Add(_mesh.nodes[relative].position); + } + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + + return _adjacentEdges; + } + + private void FillAdjacentPositions(int nodeIndex, int nodeAvoid, HashSet output) + { + output.Clear(); + + int posToAvoid = _mesh.nodes[nodeAvoid].position; + + int sibling = nodeIndex; + do + { + for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;) + { + if (_mesh.nodes[relative].position != posToAvoid) + { + output.Add(_mesh.nodes[relative].position); + } + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + } + + private void FillAdjacentPositionsByPos(int nodeIndex, int posToAvoid, HashSet output) + { + output.Clear(); + + int sibling = nodeIndex; + do + { + for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;) + { + int pos = _mesh.nodes[relative].position; + if (pos != posToAvoid) + { + output.Add(pos); + } + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + } + + private double GetEdgeTopo(EdgeCollapse edge) + { + if (edge.Weight == -1) + { + edge.SetWeight(_mesh.GetEdgeTopo(_mesh.PositionToNode[edge.posA], _mesh.PositionToNode[edge.posB])); + } + return edge.Weight; + } + + public static bool UseEdgeLength = true; + + private void CalculateError(EdgeCollapse pair) + { + Debug.Assert(_mesh.CheckEdge(_mesh.PositionToNode[pair.posA], _mesh.PositionToNode[pair.posB])); + + Vector3 posA = _mesh.positions[pair.posA]; + Vector3 posB = _mesh.positions[pair.posB]; + _evaluatedEdges++; + + if (ShouldBlockBoneWeightCollapse(pair.posA, pair.posB)) + { + _rejectedBoneWeights++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + if (ShouldBlockNormalCollapse(pair.posA, pair.posB)) + { + _rejectedTopology++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + if (ShouldBlockUvCollapse(pair.posA, pair.posB)) + { + _rejectedTopology++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + if (IsProtectedVertex(pair.posA) || IsProtectedVertex(pair.posB)) + { + _rejectedBodyCollision++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + + var edgeTopo = GetEdgeTopo(pair); + if (edgeTopo > 0d && !AllowBoundaryCollapses) + { + _rejectedTopology++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + Vector3 posC = (posB + posA) / 2; + + int nodeA = _mesh.PositionToNode[pair.posA]; + int nodeB = _mesh.PositionToNode[pair.posB]; + if (!CollapsePreservesTopology(pair)) + { + _rejectedTopology++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + if (!AllowBoundaryCollapses && (IsBoundaryVertex(nodeA) || IsBoundaryVertex(nodeB))) + { + _rejectedTopology++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + + double errorCollapseToO; + Vector3 posO = Vector3.PositiveInfinity; + + // If a node is smooth (no hard edge connected, no uv break or no border), we can compute a quadric error + // Otherwise, we add up linear errors for every non smooth source. + // If both nodes of the edge are smooth, we can find the optimal position to collapse to by inverting the + // quadric matrix, otherwise, we pick the best between A, B, and the position in the middle, C. + + SymmetricMatrix q = _matrices[pair.posA] + _matrices[pair.posB]; + double det = q.DeterminantXYZ(); + + if (det > _DeterminantEpsilon || det < -_DeterminantEpsilon) + { + posO = new Vector3( + -1d / det * q.DeterminantX(), + +1d / det * q.DeterminantY(), + -1d / det * q.DeterminantZ()); + errorCollapseToO = ComputeVertexError(q, posO.x, posO.y, posO.z); + } + else + { + errorCollapseToO = _OFFSET_NOCOLLAPSE; + } + + double errorCollapseToA = ComputeVertexError(q, posA.x, posA.y, posA.z); + double errorCollapseToB = ComputeVertexError(q, posB.x, posB.y, posB.z); + double errorCollapseToC = ComputeVertexError(q, posC.x, posC.y, posC.z); + + int pA = _mesh.nodes[nodeA].position; + int pB = _mesh.nodes[nodeB].position; + + // We multiply by edge length to be agnotics with quadrics error. + // Otherwise it becomes too scale dependent + double length = (posB - posA).Length; + if (LimitCollapseEdgeLength && length > MaxCollapseEdgeLength) + { + _rejectedTopology++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + + foreach (int pD in GetAdjacentPositions(nodeA, nodeB)) + { + Vector3 posD = _mesh.positions[pD]; + EdgeCollapse edge = new EdgeCollapse(pA, pD); + if (_pairs.TryGetValue(edge, out EdgeCollapse realEdge)) + { + double weight = GetEdgeTopo(realEdge); + errorCollapseToB += weight * length * ComputeLineicError(posB, posD, posA); + errorCollapseToC += weight * length * ComputeLineicError(posC, posD, posA); + } + } + + foreach (int pD in GetAdjacentPositions(nodeB, nodeA)) + { + Vector3 posD = _mesh.positions[pD]; + EdgeCollapse edge = new EdgeCollapse(pB, pD); + if (_pairs.TryGetValue(edge, out EdgeCollapse realEdge)) + { + double weight = GetEdgeTopo(realEdge); + errorCollapseToA += weight * length * ComputeLineicError(posA, posD, posB); + errorCollapseToC += weight * length * ComputeLineicError(posC, posD, posB); + } + } + + errorCollapseToC *= CollapseToMidpointPenalty; + + if (CollapseToEndpointsOnly) + { + errorCollapseToO = _OFFSET_NOCOLLAPSE; + errorCollapseToC = _OFFSET_NOCOLLAPSE; + } + + if (CollapseToEndpointsOnly && _bodyDistanceSq != null && _bodyDistanceThresholdSq > 0f) + { + var hasA = TryGetBodyDistanceSq(pair.posA, out var distASq); + var hasB = TryGetBodyDistanceSq(pair.posB, out var distBSq); + var nearA = hasA && distASq <= _bodyDistanceThresholdSq; + var nearB = hasB && distBSq <= _bodyDistanceThresholdSq; + + if (nearA && nearB) + { + if (distASq > distBSq) + { + errorCollapseToB = _OFFSET_NOCOLLAPSE; + } + else if (distBSq > distASq) + { + errorCollapseToA = _OFFSET_NOCOLLAPSE; + } + else + { + errorCollapseToA = _OFFSET_NOCOLLAPSE; + errorCollapseToB = _OFFSET_NOCOLLAPSE; + } + } + else + { + if (nearA) + { + errorCollapseToA = _OFFSET_NOCOLLAPSE; + } + + if (nearB) + { + errorCollapseToB = _OFFSET_NOCOLLAPSE; + } + } + + if (hasA && hasB) + { + if (distASq > distBSq) + { + errorCollapseToB = _OFFSET_NOCOLLAPSE; + } + else if (distBSq > distASq) + { + errorCollapseToA = _OFFSET_NOCOLLAPSE; + } + } + + if (errorCollapseToA >= _OFFSET_NOCOLLAPSE && errorCollapseToB >= _OFFSET_NOCOLLAPSE) + { + _rejectedBodyCollision++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + } + + if (!CollapseToEndpointsOnly && IsPointNearBody((posA + posB) * 0.5)) + { + _rejectedBodyCollision++; + pair.error = _OFFSET_NOCOLLAPSE; + return; + } + + MathUtils.SelectMin( + errorCollapseToO, errorCollapseToA, errorCollapseToB, errorCollapseToC, + posO, posA, posB, posC, + out pair.error, out pair.result); + + pair.error = Math.Max(0d, pair.error); + + if (!CollapseWillInvert(pair)) + { + pair.error = _OFFSET_NOCOLLAPSE; + } + + // TODO : Make it insensitive to model scale + } + + private bool CollapsePreservesTopology(EdgeCollapse edge) + { + int nodeIndexA = _mesh.PositionToNode[edge.posA]; + int nodeIndexB = _mesh.PositionToNode[edge.posB]; + if (nodeIndexA < 0 || nodeIndexB < 0) + { + return true; + } + + FillAdjacentPositions(nodeIndexA, nodeIndexB, _adjacentEdgesA); + FillAdjacentPositions(nodeIndexB, nodeIndexA, _adjacentEdgesB); + + int shared = 0; + foreach (var neighbor in _adjacentEdgesA) + { + if (_adjacentEdgesB.Contains(neighbor)) + { + shared++; + if (shared > 2) + { + return false; + } + } + } + + return AllowBoundaryCollapses ? shared >= 1 : shared == 2; + } + + private bool IsBoundaryVertex(int nodeIndex) + { + if (nodeIndex < 0) + { + return false; + } + + int sibling = nodeIndex; + do + { + for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;) + { + if (_mesh.GetEdgeTopo(sibling, relative) >= ConnectedMesh.EdgeBorderPenalty) + { + return true; + } + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + + return false; + } + + private bool ShouldBlockBoneWeightCollapse(int posA, int posB) + { + if (_mesh.attributes is not MetaAttributeList attrList) + { + return false; + } + + int nodeA = _mesh.PositionToNode[posA]; + int nodeB = _mesh.PositionToNode[posB]; + if (nodeA < 0 || nodeB < 0) + { + return false; + } + + bool hasWeights = false; + int siblingA = nodeA; + do + { + var attrA = (MetaAttribute)attrList[_mesh.nodes[siblingA].attribute]; + if ((attrA.attr0.flags & FfxivAttributeFlags.BoneWeights) != 0) + { + hasWeights = true; + int siblingB = nodeB; + do + { + var attrB = (MetaAttribute)attrList[_mesh.nodes[siblingB].attribute]; + if ((attrB.attr0.flags & FfxivAttributeFlags.BoneWeights) != 0 + && HasMatchingDominantBone(attrA.attr0.boneWeight, attrB.attr0.boneWeight) + && GetBoneWeightOverlapNormalized(attrA.attr0.boneWeight, attrB.attr0.boneWeight) >= BoneWeightSimilarityThreshold) + { + return false; + } + } while ((siblingB = _mesh.nodes[siblingB].sibling) != nodeB); + } + } while ((siblingA = _mesh.nodes[siblingA].sibling) != nodeA); + + return hasWeights; + } + + private bool ShouldBlockUvCollapse(int posA, int posB) + { + if (_mesh.attributes is not MetaAttributeList attrList) + { + return false; + } + + var attrA = ((MetaAttribute)attrList[posA]).attr0; + var attrB = ((MetaAttribute)attrList[posB]).attr0; + var flags = attrA.flags | attrB.flags; + if ((flags & FfxivAttributeFlags.Uv0) == 0) + { + return false; + } + + var isSeam = IsUvSeamEdge(attrA.uv0, attrB.uv0); + if (!isSeam) + { + if (BlockUvSeamVertices && (HasUvSeamAtVertex(posA, posB, attrList, attrA) || HasUvSeamAtVertex(posB, posA, attrList, attrB))) + { + return true; + } + + return false; + } + + if (!CheckUvSeamAngleAtVertex(posA, posB, attrList, attrA, attrB)) + { + return true; + } + + if (!CheckUvSeamAngleAtVertex(posB, posA, attrList, attrB, attrA)) + { + return true; + } + + return false; + } + + private bool ShouldBlockNormalCollapse(int posA, int posB) + { + if (_mesh.attributes is not MetaAttributeList attrList) + { + return false; + } + + var attrA = ((MetaAttribute)attrList[posA]).attr0; + var attrB = ((MetaAttribute)attrList[posB]).attr0; + if ((attrA.flags & FfxivAttributeFlags.Normal) == 0 || (attrB.flags & FfxivAttributeFlags.Normal) == 0) + { + return false; + } + + var dot = Vector3F.Dot(attrA.normal, attrB.normal); + return dot < _normalSimilarityThresholdCos; + } + + private static float UvDistanceSq(in Vector2F a, in Vector2F b) + { + var dx = a.x - b.x; + var dy = a.y - b.y; + return (dx * dx) + (dy * dy); + } + + private static bool IsUvSeamEdge(in Vector2F uvA, in Vector2F uvB) + { + var thresholdSq = UvSimilarityThreshold * UvSimilarityThreshold; + return UvDistanceSq(uvA, uvB) > thresholdSq; + } + + private bool HasUvSeamAtVertex(int posCenter, int posExclude, MetaAttributeList attrList, in FfxivVertexAttribute attrCenter) + { + int nodeCenter = _mesh.PositionToNode[posCenter]; + if (nodeCenter < 0) + { + return false; + } + + FillAdjacentPositionsByPos(nodeCenter, posExclude, _adjacentEdges); + foreach (int neighborPos in _adjacentEdges) + { + var attrNeighbor = ((MetaAttribute)attrList[neighborPos]).attr0; + if (((attrNeighbor.flags | attrCenter.flags) & FfxivAttributeFlags.Uv0) == 0) + { + continue; + } + + if (IsUvSeamEdge(attrCenter.uv0, attrNeighbor.uv0)) + { + return true; + } + } + + return false; + } + + private bool CheckUvSeamAngleAtVertex(int posCenter, int posOther, MetaAttributeList attrList, in FfxivVertexAttribute attrCenter, in FfxivVertexAttribute attrOther) + { + int nodeCenter = _mesh.PositionToNode[posCenter]; + if (nodeCenter < 0) + { + return true; + } + + FillAdjacentPositionsByPos(nodeCenter, posOther, _adjacentEdges); + + int seamEdges = 1; + int otherSeamPos = -1; + + foreach (int neighborPos in _adjacentEdges) + { + var attrNeighbor = ((MetaAttribute)attrList[neighborPos]).attr0; + if (((attrNeighbor.flags | attrCenter.flags) & FfxivAttributeFlags.Uv0) == 0) + { + continue; + } + + if (IsUvSeamEdge(attrCenter.uv0, attrNeighbor.uv0)) + { + seamEdges++; + otherSeamPos = neighborPos; + if (seamEdges > 2) + { + return false; + } + } + } + + if (otherSeamPos < 0) + { + return true; + } + + var attrOtherSeam = ((MetaAttribute)attrList[otherSeamPos]).attr0; + if (!TryNormalizeUvDirection(attrCenter.uv0, attrOther.uv0, out var dir1) + || !TryNormalizeUvDirection(attrCenter.uv0, attrOtherSeam.uv0, out var dir2)) + { + return false; + } + + var dot = (dir1.x * dir2.x) + (dir1.y * dir2.y); + return dot >= UvSeamAngleCos; + } + + private static bool TryNormalizeUvDirection(in Vector2F from, in Vector2F to, out Vector2F direction) + { + var dx = to.x - from.x; + var dy = to.y - from.y; + var lenSq = (dx * dx) + (dy * dy); + if (lenSq <= _UvDirEpsilonSq) + { + direction = default; + return false; + } + + var invLen = 1f / MathF.Sqrt(lenSq); + direction = new Vector2F(dx * invLen, dy * invLen); + return true; + } + + private bool TryGetBodyDistanceSq(int pos, out float distanceSq) + { + distanceSq = float.NaN; + if (_bodyDistanceSq == null) + { + return false; + } + + if ((uint)pos >= (uint)_bodyDistanceSq.Length) + { + return false; + } + + distanceSq = _bodyDistanceSq[pos]; + return !float.IsNaN(distanceSq); + } + + private static float GetBoneWeightOverlapNormalized(in BoneWeight a, in BoneWeight b) + { + var overlap = GetBoneWeightOverlap(a, b); + var sumA = GetBoneWeightSum(a); + var sumB = GetBoneWeightSum(b); + var denom = MathF.Max(sumA, sumB); + if (denom <= 1e-6f) + { + return 1f; + } + + return overlap / denom; + } + + private static bool HasMatchingDominantBone(in BoneWeight a, in BoneWeight b) + { + var dominantA = GetDominantBoneIndex(a); + if (dominantA < 0) + { + return true; + } + + var dominantB = GetDominantBoneIndex(b); + if (dominantB < 0) + { + return true; + } + + return dominantA == dominantB; + } + + private static int GetDominantBoneIndex(in BoneWeight weight) + { + var max = weight.weight0; + var index = weight.index0; + + if (weight.weight1 > max) + { + max = weight.weight1; + index = weight.index1; + } + if (weight.weight2 > max) + { + max = weight.weight2; + index = weight.index2; + } + if (weight.weight3 > max) + { + max = weight.weight3; + index = weight.index3; + } + + return max > 0f ? index : -1; + } + + private static float GetBoneWeightOverlap(in BoneWeight a, in BoneWeight b) + { + float overlap = 0f; + AddSharedWeight(a.index0, a.weight0, b, ref overlap); + AddSharedWeight(a.index1, a.weight1, b, ref overlap); + AddSharedWeight(a.index2, a.weight2, b, ref overlap); + AddSharedWeight(a.index3, a.weight3, b, ref overlap); + return overlap; + } + + private static float GetBoneWeightSum(in BoneWeight weight) + => weight.weight0 + weight.weight1 + weight.weight2 + weight.weight3; + + private static void AddSharedWeight(int index, float weight, in BoneWeight other, ref float overlap) + { + if (weight <= 0f) + { + return; + } + + if (index == other.index0) + { + overlap += MathF.Min(weight, other.weight0); + } + else if (index == other.index1) + { + overlap += MathF.Min(weight, other.weight1); + } + else if (index == other.index2) + { + overlap += MathF.Min(weight, other.weight2); + } + else if (index == other.index3) + { + overlap += MathF.Min(weight, other.weight3); + } + } + + // TODO : Fix this (doesn't seems to work properly + public bool CollapseWillInvert(EdgeCollapse edge) + { + int nodeIndexA = _mesh.PositionToNode[edge.posA]; + int nodeIndexB = _mesh.PositionToNode[edge.posB]; + Vector3 positionA = _mesh.positions[edge.posA]; + Vector3 positionB = _mesh.positions[edge.posB]; + var minAreaRatioSq = _MinTriangleAreaRatio * _MinTriangleAreaRatio; + + int sibling = nodeIndexA; + do + { + int posC = _mesh.nodes[_mesh.nodes[sibling].relative].position; + int posD = _mesh.nodes[_mesh.nodes[_mesh.nodes[sibling].relative].relative].position; + + if (posC == edge.posB || posD == edge.posB) + { + continue; + } + + Vector3F edgeAC = _mesh.positions[posC] - positionA; + Vector3F edgeAD = _mesh.positions[posD] - positionA; + Vector3F edgeCD = _mesh.positions[posD] - _mesh.positions[posC]; + var normalBefore = Vector3F.Cross(edgeAC, edgeAD); + + Vector3F edgeRC = _mesh.positions[posC] - edge.result; + Vector3F edgeRD = _mesh.positions[posD] - edge.result; + var normalAfter = Vector3F.Cross(edgeRC, edgeRD); + if (ShouldRejectBodyTriangle(edge.result, _mesh.positions[posC], _mesh.positions[posD])) + { + _rejectedBodyCollision++; + return false; + } + if (IsDegenerateTriangle(edgeAC, edgeAD, edgeCD, normalBefore) + || IsDegenerateTriangle(edgeRC, edgeRD, edgeCD, normalAfter)) + { + _rejectedDegenerate++; + _rejectedInversion++; + return false; + } + if (normalAfter.SqrMagnitude < normalBefore.SqrMagnitude * minAreaRatioSq) + { + _rejectedArea++; + _rejectedInversion++; + return false; + } + + var dot = Vector3F.Dot(normalBefore, normalAfter); + if (dot <= 0f) + { + _rejectedFlip++; + _rejectedInversion++; + return false; + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexA); + + sibling = nodeIndexB; + do + { + int posC = _mesh.nodes[_mesh.nodes[sibling].relative].position; + int posD = _mesh.nodes[_mesh.nodes[_mesh.nodes[sibling].relative].relative].position; + + if (posC == edge.posA || posD == edge.posA) + { + continue; + } + + Vector3F edgeAC = _mesh.positions[posC] - positionB; + Vector3F edgeAD = _mesh.positions[posD] - positionB; + Vector3F edgeCD = _mesh.positions[posD] - _mesh.positions[posC]; + var normalBefore = Vector3F.Cross(edgeAC, edgeAD); + + Vector3F edgeRC = _mesh.positions[posC] - edge.result; + Vector3F edgeRD = _mesh.positions[posD] - edge.result; + var normalAfter = Vector3F.Cross(edgeRC, edgeRD); + if (ShouldRejectBodyTriangle(edge.result, _mesh.positions[posC], _mesh.positions[posD])) + { + _rejectedBodyCollision++; + return false; + } + if (IsDegenerateTriangle(edgeAC, edgeAD, edgeCD, normalBefore) + || IsDegenerateTriangle(edgeRC, edgeRD, edgeCD, normalAfter)) + { + _rejectedDegenerate++; + _rejectedInversion++; + return false; + } + if (normalAfter.SqrMagnitude < normalBefore.SqrMagnitude * minAreaRatioSq) + { + _rejectedArea++; + _rejectedInversion++; + return false; + } + + var dot = Vector3F.Dot(normalBefore, normalAfter); + if (dot <= 0f) + { + _rejectedFlip++; + _rejectedInversion++; + return false; + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexB); + + return true; + } + + /// + /// A |\ + /// | \ + /// |__\ X + /// | / + /// | / + /// B |/ + /// + /// + /// + /// + /// + private double ComputeLineicError(in Vector3 A, in Vector3 B, in Vector3 X) + { + return Vector3.DistancePointLine(X, A, B); + } + + private double ComputeVertexError(in SymmetricMatrix q, double x, double y, double z) + { + return q.m0 * x * x + 2 * q.m1 * x * y + 2 * q.m2 * x * z + 2 * q.m3 * x + + q.m4 * y * y + 2 * q.m5 * y * z + 2 * q.m6 * y + + q.m7 * z * z + 2 * q.m8 * z + + q.m9; + } + + private void InterpolateAttributes(EdgeCollapse pair) + { + int posA = pair.posA; + int posB = pair.posB; + + int nodeIndexA = _mesh.PositionToNode[posA]; + int nodeIndexB = _mesh.PositionToNode[posB]; + + Vector3 positionA = _mesh.positions[posA]; + Vector3 positionB = _mesh.positions[posB]; + + HashSet procAttributes = new HashSet(); + + Vector3 positionN = pair.result; + double AN = Vector3.Magnitude(positionA - positionN); + double BN = Vector3.Magnitude(positionB - positionN); + double ratio = MathUtils.DivideSafe(AN, AN + BN); + + /* // Other way (same results I think) + double ratio = 0; + double dot = Vector3.Dot(pair.result - positionA, positionB - positionA); + if (dot > 0) + ratio = Math.Sqrt(dot); + ratio /= (positionB - positionA).Length; + */ + + // TODO : Probleme d'interpolation + + + int siblingOfA = nodeIndexA; + do // Iterator over faces around A + { + int relativeOfA = siblingOfA; + do // Circulate around face + { + if (_mesh.nodes[relativeOfA].position == posB) + { + if (!procAttributes.Add(_mesh.nodes[siblingOfA].attribute)) + continue; + + if (!procAttributes.Add(_mesh.nodes[relativeOfA].attribute)) + continue; + + if (_mesh.attributes != null && _mesh.attributeDefinitions.Length > 0) + { + IMetaAttribute attributeA = _mesh.attributes[_mesh.nodes[siblingOfA].attribute]; + IMetaAttribute attributeB = _mesh.attributes[_mesh.nodes[relativeOfA].attribute]; + + for (int i = 0; i < _mesh.attributeDefinitions.Length; i++) + { + if (_mesh.attributeDefinitions[i].type == AttributeType.Normals) + { + Vector3F normalA = attributeA.Get(i); + Vector3F normalB = attributeB.Get(i); + + float dot = Vector3F.Dot(normalA, normalB); + + if (dot < _mergeNormalsThresholdCos) + { + continue; + } + } + + _mesh.attributes.Interpolate(i, _mesh.nodes[siblingOfA].attribute, _mesh.nodes[relativeOfA].attribute, ratio); + } + } + } + } while ((relativeOfA = _mesh.nodes[relativeOfA].relative) != siblingOfA); + + } while ((siblingOfA = _mesh.nodes[siblingOfA].sibling) != nodeIndexA); + + + /* + int attrIndex = _mesh.nodes[nodeIndexA].attribute; + + int siblingOfA = nodeIndexA; + do + { + _mesh.nodes[siblingOfA].attribute = attrIndex; + } while ((siblingOfA = _mesh.nodes[siblingOfA].sibling) != nodeIndexA); + + int siblingOfB = nodeIndexB; + do + { + _mesh.nodes[siblingOfB].attribute = attrIndex; + } while ((siblingOfB = _mesh.nodes[siblingOfB].sibling) != nodeIndexB); + */ + } + + private readonly Dictionary _uniqueAttributes = new Dictionary(); + + private void MergeAttributes(int nodeIndex) + { + if (_mesh.attributeDefinitions.Length == 0) + return; + + _uniqueAttributes.Clear(); + + int sibling = nodeIndex; + do + { + _uniqueAttributes.TryAdd(_mesh.attributes[_mesh.nodes[sibling].attribute], _mesh.nodes[sibling].attribute); + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + + sibling = nodeIndex; + do + { + _mesh.nodes[sibling].attribute = _uniqueAttributes[_mesh.attributes[_mesh.nodes[sibling].attribute]]; + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex); + } + + private readonly HashSet _edgeToRefresh = new HashSet(); + + private void CollapseEdge(EdgeCollapse pair) + { + _collapsedEdges++; + int nodeIndexA = _mesh.PositionToNode[pair.posA]; + int nodeIndexB = _mesh.PositionToNode[pair.posB]; + + int posA = pair.posA; + int posB = pair.posB; + + // Remove all edges around A + int sibling = nodeIndexA; + //for (relative = sibling; relative != sibling; relative = _mesh.nodes[relative].relative) + //for (sibling = nodeIndexA; sibling != nodeIndexA; sibling = _mesh.nodes[sibling].sibling) + do + { + for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;) + { + int posC = _mesh.nodes[relative].position; + EdgeCollapse pairAC = new EdgeCollapse(posA, posC); + // Todo : Optimization by only removing first pair (first edge) + if (_pairs.Remove(pairAC)) + { + _mins.Remove(pairAC); + } + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexA); + + // Remove all edges around B + sibling = nodeIndexB; + do + { + for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;) + { + int posC = _mesh.nodes[relative].position; + EdgeCollapse pairBC = new EdgeCollapse(posB, posC); + if (_pairs.Remove(pairBC)) + { + _mins.Remove(pairBC); + } + } + } while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexB); + + // Interpolates attributes + InterpolateAttributes(pair); + + // Collapse edge + int validNode = _mesh.CollapseEdge(nodeIndexA, nodeIndexB); + + // A disconnected triangle has been collapsed, there are no edges to register + if (validNode < 0) + { + return; + } + + posA = _mesh.nodes[validNode].position; + + _mesh.positions[posA] = pair.result; + + MergeAttributes(validNode); + + CalculateQuadric(posA); + + _edgeToRefresh.Clear(); + + sibling = validNode; + do + { + for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;) + { + int posC = _mesh.nodes[relative].position; + _edgeToRefresh.Add(new EdgeCollapse(posA, posC)); + + if (UpdateFarNeighbors) + { + int sibling2 = relative; + while ((sibling2 = _mesh.nodes[sibling2].sibling) != relative) + { + int relative2 = sibling2; + while ((relative2 = _mesh.nodes[relative2].relative) != sibling2) + { + int posD = _mesh.nodes[relative2].position; + if (posD != posC) + { + _edgeToRefresh.Add(new EdgeCollapse(posC, posD)); + } + } + } + } + } + } while ((sibling = _mesh.nodes[sibling].sibling) != validNode); + + foreach (EdgeCollapse edge in _edgeToRefresh) + { + CalculateQuadric(edge.posB); + edge.SetWeight(-1); + _pairs.Remove(edge); + _pairs.Add(edge); + } + + foreach (EdgeCollapse edge in _edgeToRefresh) + { + CalculateError(edge); + _mins.Remove(edge); + if (UpdateMinsOnCollapse) + { + _mins.AddMin(edge); + } + } + } + + private void ResetStats() + { + _evaluatedEdges = 0; + _collapsedEdges = 0; + _rejectedBoneWeights = 0; + _rejectedTopology = 0; + _rejectedInversion = 0; + _rejectedDegenerate = 0; + _rejectedArea = 0; + _rejectedFlip = 0; + _rejectedBodyCollision = 0; + } + + private bool IsPointNearBody(in Vector3 point) + { + if (_bodyDistanceSqEvaluator == null || _bodyDistanceThresholdSq <= 0f) + { + return false; + } + + var sq = _bodyDistanceSqEvaluator(point); + return !float.IsNaN(sq) && sq <= _bodyDistanceThresholdSq; + } + + private bool IsPointNearBody(in Vector3 point, float thresholdSq) + { + if (_bodyDistanceSqEvaluator == null || thresholdSq <= 0f) + { + return false; + } + + var sq = _bodyDistanceSqEvaluator(point); + return !float.IsNaN(sq) && sq <= thresholdSq; + } + + private bool ShouldRejectBodyTriangle(in Vector3 a, in Vector3 b, in Vector3 c) + { + if (_bodyDistanceSqEvaluator == null || _bodyDistanceThresholdSq <= 0f) + { + return false; + } + + var centroid = (a + b + c) / 3d; + if (!CollapseToEndpointsOnly) + { + return IsPointNearBody(centroid); + } + + var penetrationFactor = MathF.Max(0f, BodyCollisionPenetrationFactor); + var penetrationThresholdSq = _bodyDistanceThresholdSq * penetrationFactor * penetrationFactor; + if (IsPointNearBody(centroid, penetrationThresholdSq)) + { + return true; + } + + var ab = (a + b) * 0.5; + var bc = (b + c) * 0.5; + var ca = (c + a) * 0.5; + return IsPointNearBody(ab, penetrationThresholdSq) + || IsPointNearBody(bc, penetrationThresholdSq) + || IsPointNearBody(ca, penetrationThresholdSq); + } + + private bool IsProtectedVertex(int pos) + { + if (_protectedVertices == null) + { + return false; + } + + return (uint)pos < (uint)_protectedVertices.Length && _protectedVertices[pos]; + } + + private static bool IsDegenerateTriangle(in Vector3F edge0, in Vector3F edge1, in Vector3F edge2, in Vector3F normal) + { + var maxEdgeSq = MathF.Max(edge0.SqrMagnitude, MathF.Max(edge1.SqrMagnitude, edge2.SqrMagnitude)); + if (maxEdgeSq <= 0f) + { + return true; + } + + var minNormalSq = (float)(_DeterminantEpsilon * _DeterminantEpsilon) * maxEdgeSq * maxEdgeSq; + return normal.SqrMagnitude <= minNormalSq; + } + } + + public readonly record struct DecimationStats( + int EvaluatedEdges, + int CollapsedEdges, + int RejectedBoneWeights, + int RejectedTopology, + int RejectedInversion, + int RejectedDegenerate, + int RejectedArea, + int RejectedFlip, + int RejectedBodyCollision); +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeCollapse.cs b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeCollapse.cs new file mode 100644 index 0000000..62cae64 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeCollapse.cs @@ -0,0 +1,88 @@ +using System; + +namespace Nanomesh +{ + public partial class DecimateModifier + { + public class EdgeCollapse : IComparable, IEquatable + { + public int posA; + public int posB; + public Vector3 result; + public double error; + + private double _weight = -1; + + public ref double Weight => ref _weight; + + public void SetWeight(double weight) + { + _weight = weight; + } + + public EdgeCollapse(int posA, int posB) + { + this.posA = posA; + this.posB = posB; + } + + public override int GetHashCode() + { + unchecked + { + return posA + posB; + } + } + + public override bool Equals(object obj) + { + return Equals((EdgeCollapse)obj); + } + + public bool Equals(EdgeCollapse pc) + { + if (ReferenceEquals(pc, null)) + return false; + + if (ReferenceEquals(this, pc)) + { + return true; + } + else + { + return (posA == pc.posA && posB == pc.posB) || (posA == pc.posB && posB == pc.posA); + } + } + + public int CompareTo(EdgeCollapse other) + { + return error > other.error ? 1 : error < other.error ? -1 : 0; + } + + public static bool operator >(EdgeCollapse x, EdgeCollapse y) + { + return x.error > y.error; + } + + public static bool operator >=(EdgeCollapse x, EdgeCollapse y) + { + return x.error >= y.error; + } + + public static bool operator <(EdgeCollapse x, EdgeCollapse y) + { + return x.error < y.error; + } + + public static bool operator <=(EdgeCollapse x, EdgeCollapse y) + { + return x.error <= y.error; + } + + public override string ToString() + { + return $""; + } + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeComparer.cs b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeComparer.cs new file mode 100644 index 0000000..4fb45e6 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeComparer.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Nanomesh +{ + public partial class DecimateModifier + { + private class EdgeComparer : IComparer + { + public int Compare(EdgeCollapse x, EdgeCollapse y) + { + return x.CompareTo(y); + } + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/SceneDecimator.cs b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/SceneDecimator.cs new file mode 100644 index 0000000..c21cbc2 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/SceneDecimator.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Nanomesh +{ + public class SceneDecimator + { + private class ModifierAndOccurrences + { + public int occurrences = 1; + public DecimateModifier modifier = new DecimateModifier(); + } + + private Dictionary _modifiers; + + public void Initialize(IEnumerable meshes) + { + _modifiers = new Dictionary(); + + foreach (ConnectedMesh mesh in meshes) + { + ModifierAndOccurrences modifier; + if (_modifiers.ContainsKey(mesh)) + { + modifier = _modifiers[mesh]; + modifier.occurrences++; + } + else + { + _modifiers.Add(mesh, modifier = new ModifierAndOccurrences()); + //System.Console.WriteLine($"Faces:{mesh.FaceCount}"); + modifier.modifier.Initialize(mesh); + } + + _faceCount += mesh.FaceCount; + } + + _initalFaceCount = _faceCount; + } + + private int _faceCount; + private int _initalFaceCount; + + public void DecimateToRatio(float targetTriangleRatio) + { + targetTriangleRatio = MathF.Clamp(targetTriangleRatio, 0f, 1f); + DecimateToPolycount((int)MathF.Round(targetTriangleRatio * _initalFaceCount)); + } + + public void DecimatePolycount(int polycount) + { + DecimateToPolycount((int)MathF.Round(_initalFaceCount - polycount)); + } + + public void DecimateToPolycount(int targetTriangleCount) + { + //System.Console.WriteLine($"Faces:{_faceCount} Target:{targetTriangleCount}"); + while (_faceCount > targetTriangleCount) + { + KeyValuePair pair = _modifiers.OrderBy(x => x.Value.modifier.GetMinimumError()).First(); + + int facesBefore = pair.Key.FaceCount; + pair.Value.modifier.Iterate(); + + if (facesBefore == pair.Key.FaceCount) + break; // Exit ! + + _faceCount -= (facesBefore - pair.Key.FaceCount) * pair.Value.occurrences; + } + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Algo/NormalsCreator.cs b/LightlessSync/ThirdParty/Nanomesh/Algo/NormalsCreator.cs new file mode 100644 index 0000000..5c37321 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Algo/NormalsCreator.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Nanomesh +{ + public class NormalsModifier + { + public struct PosAndAttribute : IEquatable + { + public int position; + public Attribute attribute; + + public override int GetHashCode() + { + return position.GetHashCode() ^ (attribute.GetHashCode() << 2); + } + + public bool Equals(PosAndAttribute other) + { + return position == other.position && attribute.Equals(other.attribute); + } + } + + public void Run(ConnectedMesh mesh, float smoothingAngle) + { + float cosineThreshold = MathF.Cos(smoothingAngle * MathF.PI / 180f); + + int[] positionToNode = mesh.GetPositionToNode(); + + Dictionary attributeToIndex = new Dictionary(); + + for (int p = 0; p < positionToNode.Length; p++) + { + int nodeIndex = positionToNode[p]; + if (nodeIndex < 0) + { + continue; + } + + Debug.Assert(!mesh.nodes[nodeIndex].IsRemoved); + + int sibling1 = nodeIndex; + do + { + Vector3F sum = Vector3F.Zero; + + Vector3F normal1 = mesh.GetFaceNormal(sibling1); + + int sibling2 = nodeIndex; + do + { + Vector3F normal2 = mesh.GetFaceNormal(sibling2); + + float dot = Vector3F.Dot(normal1, normal2); + + if (dot >= cosineThreshold) + { + // Area and angle weighting (it gives better results) + sum += mesh.GetFaceArea(sibling2) * mesh.GetAngleRadians(sibling2) * normal2; + } + + } while ((sibling2 = mesh.nodes[sibling2].sibling) != nodeIndex); + + sum = sum.Normalized; + + + } while ((sibling1 = mesh.nodes[sibling1].sibling) != nodeIndex); + } + + // Assign new attributes + + // TODO : Fix + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Algo/NormalsFixer.cs b/LightlessSync/ThirdParty/Nanomesh/Algo/NormalsFixer.cs new file mode 100644 index 0000000..5e65476 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Algo/NormalsFixer.cs @@ -0,0 +1,17 @@ +namespace Nanomesh +{ + public class NormalsFixer + { + public void Start(ConnectedMesh mesh) + { + /* + for (int i = 0; i < mesh.attributes.Length; i++) + { + Attribute attribute = mesh.attributes[i]; + attribute.normal = attribute.normal.Normalized; + mesh.attributes[i] = attribute; + } + */ + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Algo/Triangulate.cs b/LightlessSync/ThirdParty/Nanomesh/Algo/Triangulate.cs new file mode 100644 index 0000000..8c69394 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Algo/Triangulate.cs @@ -0,0 +1,27 @@ +using System; + +namespace Nanomesh +{ + public class TriangulateModifier + { + public void Run(ConnectedMesh mesh) + { + for (int i = 0; i < mesh.nodes.Length; i++) + { + int edgeCount = 0; + int relative = i; + while ((relative = mesh.nodes[relative].relative) != i) // Circulate around face + { + edgeCount++; + } + + if (edgeCount > 2) + { + throw new Exception("Mesh has polygons of dimension 4 or greater"); + } + } + + // Todo : Implement + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs b/LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs new file mode 100644 index 0000000..c784572 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Nanomesh +{ + public readonly struct BoneWeight : IEquatable, IInterpolable + { + public readonly int index0; + public readonly int index1; + public readonly int index2; + public readonly int index3; + public readonly float weight0; + public readonly float weight1; + public readonly float weight2; + public readonly float weight3; + + public int GetIndex(int i) + { + switch (i) + { + case 0: return index0; + case 1: return index1; + case 2: return index2; + case 3: return index3; + default: return -1; + } + } + + public float GetWeight(int i) + { + switch (i) + { + case 0: return weight0; + case 1: return weight1; + case 2: return weight2; + case 3: return weight3; + default: return -1; + } + } + + public BoneWeight(int index0, int index1, int index2, int index3, float weight0, float weight1, float weight2, float weight3) + { + this.index0 = index0; + this.index1 = index1; + this.index2 = index2; + this.index3 = index3; + this.weight0 = weight0; + this.weight1 = weight1; + this.weight2 = weight2; + this.weight3 = weight3; + } + + public bool Equals(BoneWeight other) + { + return index0 == other.index0 + && index1 == other.index1 + && index2 == other.index2 + && index3 == other.index3 + && weight0 == other.weight0 + && weight1 == other.weight1 + && weight2 == other.weight2 + && weight3 == other.weight3; + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = hash * 31 + index0; + hash = hash * 31 + index1; + hash = hash * 31 + index2; + hash = hash * 31 + index3; + hash = hash * 31 + weight0.GetHashCode(); + hash = hash * 31 + weight1.GetHashCode(); + hash = hash * 31 + weight2.GetHashCode(); + hash = hash * 31 + weight3.GetHashCode(); + return hash; + } + } + + public unsafe BoneWeight Interpolate(BoneWeight other, double ratio) + { + BoneWeight boneWeightA = this; + BoneWeight boneWeightB = other; + + Dictionary newBoneWeight = new Dictionary(); + + // Map weights and indices + for (int i = 0; i < 4; i++) + { + newBoneWeight.TryAdd(boneWeightA.GetIndex(i), 0); + newBoneWeight.TryAdd(boneWeightB.GetIndex(i), 0); + newBoneWeight[boneWeightA.GetIndex(i)] += (float)((1 - ratio) * boneWeightA.GetWeight(i)); + newBoneWeight[boneWeightB.GetIndex(i)] += (float)(ratio * boneWeightB.GetWeight(i)); + } + + int* newIndices = stackalloc int[4]; + float* newWeights = stackalloc float[4]; + + // Order from biggest to smallest weight, and drop bones above 4th + float totalWeight = 0; + int k = 0; + foreach (KeyValuePair boneWeightN in newBoneWeight.OrderByDescending(x => x.Value)) + { + newIndices[k] = boneWeightN.Key; + newWeights[k] = boneWeightN.Value; + totalWeight += boneWeightN.Value; + if (k == 3) + break; + k++; + } + + var sumA = boneWeightA.weight0 + boneWeightA.weight1 + boneWeightA.weight2 + boneWeightA.weight3; + var sumB = boneWeightB.weight0 + boneWeightB.weight1 + boneWeightB.weight2 + boneWeightB.weight3; + var targetSum = (float)((1d - ratio) * sumA + ratio * sumB); + + // Normalize and re-scale to preserve original weight sum. + if (totalWeight > 0f) + { + var scale = targetSum / totalWeight; + for (int j = 0; j < 4; j++) + { + newWeights[j] *= scale; + } + } + + return new BoneWeight( + newIndices[0], newIndices[1], newIndices[2], newIndices[3], + newWeights[0], newWeights[1], newWeights[2], newWeights[3]); + + //return new BoneWeight( + // ratio < 0.5f ? index0 : other.index0, + // ratio < 0.5f ? index1 : other.index1, + // ratio < 0.5f ? index2 : other.index2, + // ratio < 0.5f ? index3 : other.index3, + // (float)(ratio * weight0 + (1 - ratio) * other.weight0), + // (float)(ratio * weight1 + (1 - ratio) * other.weight1), + // (float)(ratio * weight2 + (1 - ratio) * other.weight2), + // (float)(ratio * weight3 + (1 - ratio) * other.weight3)); + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs new file mode 100644 index 0000000..49a4216 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs @@ -0,0 +1,110 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nanomesh +{ + [StructLayout(LayoutKind.Explicit)] + public readonly struct Color32 : IEquatable, IInterpolable + { + [FieldOffset(0)] + internal readonly int rgba; + + [FieldOffset(0)] + public readonly byte r; + + [FieldOffset(1)] + public readonly byte g; + + [FieldOffset(2)] + public readonly byte b; + + [FieldOffset(3)] + public readonly byte a; + + public Color32(byte r, byte g, byte b, byte a) + { + rgba = 0; + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + public Color32(float r, float g, float b, float a) + { + rgba = 0; + this.r = (byte)MathF.Round(r); + this.g = (byte)MathF.Round(g); + this.b = (byte)MathF.Round(b); + this.a = (byte)MathF.Round(a); + } + + public Color32(double r, double g, double b, double a) + { + rgba = 0; + this.r = (byte)Math.Round(r); + this.g = (byte)Math.Round(g); + this.b = (byte)Math.Round(b); + this.a = (byte)Math.Round(a); + } + + public bool Equals(Color32 other) + { + return other.rgba == rgba; + } + + public Color32 Interpolate(Color32 other, double ratio) + { + return ratio * this + (1 - ratio) * other; + } + + /// + /// Adds two colors. + /// + /// + public static Color32 operator +(Color32 a, Color32 b) { return new Color32(a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a); } + + /// + /// Subtracts one color from another. + /// + /// + public static Color32 operator -(Color32 a, Color32 b) { return new Color32(1f * a.r - b.r, a.g - b.g, a.b - b.b, a.a - b.a); } + + /// + /// Multiplies one color by another. + /// + /// + public static Color32 operator *(Color32 a, Color32 b) { return new Color32(1f * a.r * b.r, 1f * a.g * b.g, 1f * a.b * b.b, 1f * a.a * b.a); } + + /// + /// Divides one color over another. + /// + /// + public static Color32 operator /(Color32 a, Color32 b) { return new Color32(1f * a.r / b.r, 1f * a.g / b.g, 1f * a.b / b.b, 1f * a.a / b.a); } + + + /// + /// Multiplies a color by a number. + /// + /// + /// + /// + public static Color32 operator *(Color32 a, float d) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); } + + public static Color32 operator *(Color32 a, double d) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); } + + /// + /// Multiplies a color by a number. + /// + /// + public static Color32 operator *(float d, Color32 a) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); } + + public static Color32 operator *(double d, Color32 a) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); } + + /// + /// Divides a color by a number. + /// + /// + public static Color32 operator /(Color32 a, float d) { return new Color32(1f * a.r / d, 1f * a.g / d, 1f * a.b / d, 1f * a.a / d); } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/FfxivVertexAttribute.cs b/LightlessSync/ThirdParty/Nanomesh/Base/FfxivVertexAttribute.cs new file mode 100644 index 0000000..f0bacb0 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/FfxivVertexAttribute.cs @@ -0,0 +1,347 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nanomesh +{ + [Flags] + public enum FfxivAttributeFlags : uint + { + None = 0, + Normal = 1u << 0, + Tangent1 = 1u << 1, + Tangent2 = 1u << 2, + Color = 1u << 3, + BoneWeights = 1u << 4, + PositionW = 1u << 5, + NormalW = 1u << 6, + Uv0 = 1u << 7, + Uv1 = 1u << 8, + Uv2 = 1u << 9, + Uv3 = 1u << 10, + } + + [StructLayout(LayoutKind.Sequential)] + public readonly struct FfxivVertexAttribute : IEquatable, IInterpolable + { + public readonly Vector3F normal; + public readonly Vector4F tangent1; + public readonly Vector4F tangent2; + public readonly Vector2F uv0; + public readonly Vector2F uv1; + public readonly Vector2F uv2; + public readonly Vector2F uv3; + public readonly Vector4F color; + public readonly BoneWeight boneWeight; + public readonly float positionW; + public readonly float normalW; + public readonly FfxivAttributeFlags flags; + + public FfxivVertexAttribute( + FfxivAttributeFlags flags, + Vector3F normal, + Vector4F tangent1, + Vector4F tangent2, + Vector2F uv0, + Vector2F uv1, + Vector2F uv2, + Vector2F uv3, + Vector4F color, + BoneWeight boneWeight, + float positionW, + float normalW) + { + this.flags = flags; + this.normal = normal; + this.tangent1 = tangent1; + this.tangent2 = tangent2; + this.uv0 = uv0; + this.uv1 = uv1; + this.uv2 = uv2; + this.uv3 = uv3; + this.color = color; + this.boneWeight = boneWeight; + this.positionW = positionW; + this.normalW = normalW; + } + + public FfxivVertexAttribute Interpolate(FfxivVertexAttribute other, double ratio) + { + var t = (float)ratio; + var inv = 1f - t; + var combinedFlags = flags | other.flags; + + var normal = (combinedFlags & FfxivAttributeFlags.Normal) != 0 + ? NormalizeVector3(new Vector3F( + (this.normal.x * inv) + (other.normal.x * t), + (this.normal.y * inv) + (other.normal.y * t), + (this.normal.z * inv) + (other.normal.z * t))) + : default; + + var tangent1 = (combinedFlags & FfxivAttributeFlags.Tangent1) != 0 + ? BlendTangent(this.tangent1, other.tangent1, t) + : default; + + var tangent2 = (combinedFlags & FfxivAttributeFlags.Tangent2) != 0 + ? BlendTangent(this.tangent2, other.tangent2, t) + : default; + + var uv0 = (combinedFlags & FfxivAttributeFlags.Uv0) != 0 + ? Vector2F.LerpUnclamped(this.uv0, other.uv0, t) + : default; + + var uv1 = (combinedFlags & FfxivAttributeFlags.Uv1) != 0 + ? Vector2F.LerpUnclamped(this.uv1, other.uv1, t) + : default; + + var uv2 = (combinedFlags & FfxivAttributeFlags.Uv2) != 0 + ? Vector2F.LerpUnclamped(this.uv2, other.uv2, t) + : default; + + var uv3 = (combinedFlags & FfxivAttributeFlags.Uv3) != 0 + ? Vector2F.LerpUnclamped(this.uv3, other.uv3, t) + : default; + + var color = (combinedFlags & FfxivAttributeFlags.Color) != 0 + ? new Vector4F( + (this.color.x * inv) + (other.color.x * t), + (this.color.y * inv) + (other.color.y * t), + (this.color.z * inv) + (other.color.z * t), + (this.color.w * inv) + (other.color.w * t)) + : default; + + var boneWeight = (combinedFlags & FfxivAttributeFlags.BoneWeights) != 0 + ? BlendBoneWeights(this.boneWeight, other.boneWeight, t) + : default; + + var positionW = (combinedFlags & FfxivAttributeFlags.PositionW) != 0 + ? (this.positionW * inv) + (other.positionW * t) + : 0f; + + var normalW = (combinedFlags & FfxivAttributeFlags.NormalW) != 0 + ? (this.normalW * inv) + (other.normalW * t) + : 0f; + + return new FfxivVertexAttribute( + combinedFlags, + normal, + tangent1, + tangent2, + uv0, + uv1, + uv2, + uv3, + color, + boneWeight, + positionW, + normalW); + } + + public bool Equals(FfxivVertexAttribute other) + { + if (flags != other.flags) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Normal) != 0 && !normal.Equals(other.normal)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Tangent1) != 0 && !tangent1.Equals(other.tangent1)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Tangent2) != 0 && !tangent2.Equals(other.tangent2)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Uv0) != 0 && !uv0.Equals(other.uv0)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Uv1) != 0 && !uv1.Equals(other.uv1)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Uv2) != 0 && !uv2.Equals(other.uv2)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Uv3) != 0 && !uv3.Equals(other.uv3)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.Color) != 0 && !color.Equals(other.color)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.BoneWeights) != 0 && !boneWeight.Equals(other.boneWeight)) + { + return false; + } + + if ((flags & FfxivAttributeFlags.PositionW) != 0 && positionW != other.positionW) + { + return false; + } + + if ((flags & FfxivAttributeFlags.NormalW) != 0 && normalW != other.normalW) + { + return false; + } + + return true; + } + + public override bool Equals(object? obj) + => obj is FfxivVertexAttribute other && Equals(other); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(normal); + hash.Add(tangent1); + hash.Add(tangent2); + hash.Add(uv0); + hash.Add(uv1); + hash.Add(uv2); + hash.Add(uv3); + hash.Add(color); + hash.Add(boneWeight); + hash.Add(positionW); + hash.Add(normalW); + hash.Add(flags); + return hash.ToHashCode(); + } + + private static Vector3F NormalizeVector3(in Vector3F value) + { + var length = Vector3F.Magnitude(value); + return length > 0f ? value / length : value; + } + + private static Vector4F BlendTangent(in Vector4F a, in Vector4F b, float t) + { + var inv = 1f - t; + var blended = new Vector3F( + (a.x * inv) + (b.x * t), + (a.y * inv) + (b.y * t), + (a.z * inv) + (b.z * t)); + blended = NormalizeVector3(blended); + + var w = t >= 0.5f ? b.w : a.w; + if (w != 0f) + { + w = w >= 0f ? 1f : -1f; + } + + return new Vector4F(blended.x, blended.y, blended.z, w); + } + + private static BoneWeight BlendBoneWeights(in BoneWeight a, in BoneWeight b, float ratio) + { + Span indices = stackalloc int[8]; + Span weights = stackalloc float[8]; + var count = 0; + + static void AddWeight(Span indices, Span weights, ref int count, int index, float weight) + { + if (weight <= 0f) + { + return; + } + + for (var i = 0; i < count; i++) + { + if (indices[i] == index) + { + weights[i] += weight; + return; + } + } + + if (count < indices.Length) + { + indices[count] = index; + weights[count] = weight; + count++; + } + } + + var inv = 1f - ratio; + var sumA = a.weight0 + a.weight1 + a.weight2 + a.weight3; + var sumB = b.weight0 + b.weight1 + b.weight2 + b.weight3; + var targetSum = (sumA * inv) + (sumB * ratio); + AddWeight(indices, weights, ref count, a.index0, a.weight0 * inv); + AddWeight(indices, weights, ref count, a.index1, a.weight1 * inv); + AddWeight(indices, weights, ref count, a.index2, a.weight2 * inv); + AddWeight(indices, weights, ref count, a.index3, a.weight3 * inv); + AddWeight(indices, weights, ref count, b.index0, b.weight0 * ratio); + AddWeight(indices, weights, ref count, b.index1, b.weight1 * ratio); + AddWeight(indices, weights, ref count, b.index2, b.weight2 * ratio); + AddWeight(indices, weights, ref count, b.index3, b.weight3 * ratio); + + if (count == 0) + { + return a; + } + + Span topIndices = stackalloc int[4]; + Span topWeights = stackalloc float[4]; + for (var i = 0; i < 4; i++) + { + topIndices[i] = -1; + topWeights[i] = 0f; + } + + for (var i = 0; i < count; i++) + { + var weight = weights[i]; + var index = indices[i]; + for (var slot = 0; slot < 4; slot++) + { + if (weight > topWeights[slot]) + { + for (var shift = 3; shift > slot; shift--) + { + topWeights[shift] = topWeights[shift - 1]; + topIndices[shift] = topIndices[shift - 1]; + } + + topWeights[slot] = weight; + topIndices[slot] = index; + break; + } + } + } + + var sum = topWeights[0] + topWeights[1] + topWeights[2] + topWeights[3]; + if (sum > 0f) + { + var scale = targetSum > 0f ? targetSum / sum : 0f; + for (var i = 0; i < 4; i++) + { + topWeights[i] *= scale; + } + } + + return new BoneWeight( + topIndices[0] < 0 ? 0 : topIndices[0], + topIndices[1] < 0 ? 0 : topIndices[1], + topIndices[2] < 0 ? 0 : topIndices[2], + topIndices[3] < 0 ? 0 : topIndices[3], + topWeights[0], + topWeights[1], + topWeights[2], + topWeights[3]); + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/IInterpolable.cs b/LightlessSync/ThirdParty/Nanomesh/Base/IInterpolable.cs new file mode 100644 index 0000000..3118194 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/IInterpolable.cs @@ -0,0 +1,7 @@ +namespace Nanomesh +{ + public interface IInterpolable + { + T Interpolate(T other, double ratio); + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs b/LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs new file mode 100644 index 0000000..c1aef5e --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs @@ -0,0 +1,356 @@ +using System; + +namespace Nanomesh +{ + public static partial class MathF + { + // Returns the sine of angle /f/ in radians. + public static float Sin(float f) { return (float)Math.Sin(f); } + + // Returns the cosine of angle /f/ in radians. + public static float Cos(float f) { return (float)Math.Cos(f); } + + // Returns the tangent of angle /f/ in radians. + public static float Tan(float f) { return (float)Math.Tan(f); } + + // Returns the arc-sine of /f/ - the angle in radians whose sine is /f/. + public static float Asin(float f) { return (float)Math.Asin(f); } + + // Returns the arc-cosine of /f/ - the angle in radians whose cosine is /f/. + public static float Acos(float f) { return (float)Math.Acos(f); } + + // Returns the arc-tangent of /f/ - the angle in radians whose tangent is /f/. + public static float Atan(float f) { return (float)Math.Atan(f); } + + // Returns the angle in radians whose ::ref::Tan is @@y/x@@. + public static float Atan2(float y, float x) { return (float)Math.Atan2(y, x); } + + // Returns square root of /f/. + public static float Sqrt(float f) { return (float)Math.Sqrt(f); } + + // Returns the absolute value of /f/. + public static float Abs(float f) { return (float)Math.Abs(f); } + + // Returns the absolute value of /value/. + public static int Abs(int value) { return Math.Abs(value); } + + /// *listonly* + public static float Min(float a, float b) { return a < b ? a : b; } + // Returns the smallest of two or more values. + public static float Min(params float[] values) + { + int len = values.Length; + if (len == 0) + { + return 0; + } + + float m = values[0]; + for (int i = 1; i < len; i++) + { + if (values[i] < m) + { + m = values[i]; + } + } + return m; + } + + /// *listonly* + public static int Min(int a, int b) { return a < b ? a : b; } + // Returns the smallest of two or more values. + public static int Min(params int[] values) + { + int len = values.Length; + if (len == 0) + { + return 0; + } + + int m = values[0]; + for (int i = 1; i < len; i++) + { + if (values[i] < m) + { + m = values[i]; + } + } + return m; + } + + /// *listonly* + public static float Max(float a, float b) { return a > b ? a : b; } + // Returns largest of two or more values. + public static float Max(params float[] values) + { + int len = values.Length; + if (len == 0) + { + return 0; + } + + float m = values[0]; + for (int i = 1; i < len; i++) + { + if (values[i] > m) + { + m = values[i]; + } + } + return m; + } + + /// *listonly* + public static int Max(int a, int b) { return a > b ? a : b; } + // Returns the largest of two or more values. + public static int Max(params int[] values) + { + int len = values.Length; + if (len == 0) + { + return 0; + } + + int m = values[0]; + for (int i = 1; i < len; i++) + { + if (values[i] > m) + { + m = values[i]; + } + } + return m; + } + + // Returns /f/ raised to power /p/. + public static float Pow(float f, float p) { return (float)Math.Pow(f, p); } + + // Returns e raised to the specified power. + public static float Exp(float power) { return (float)Math.Exp(power); } + + // Returns the logarithm of a specified number in a specified base. + public static float Log(float f, float p) { return (float)Math.Log(f, p); } + + // Returns the natural (base e) logarithm of a specified number. + public static float Log(float f) { return (float)Math.Log(f); } + + // Returns the base 10 logarithm of a specified number. + public static float Log10(float f) { return (float)Math.Log10(f); } + + // Returns the smallest integer greater to or equal to /f/. + public static float Ceil(float f) { return (float)Math.Ceiling(f); } + + // Returns the largest integer smaller to or equal to /f/. + public static float Floor(float f) { return (float)Math.Floor(f); } + + // Returns /f/ rounded to the nearest integer. + public static float Round(float f) { return (float)Math.Round(f); } + + // Returns the smallest integer greater to or equal to /f/. + public static int CeilToInt(float f) { return (int)Math.Ceiling(f); } + + // Returns the largest integer smaller to or equal to /f/. + public static int FloorToInt(float f) { return (int)Math.Floor(f); } + + // Returns /f/ rounded to the nearest integer. + public static int RoundToInt(float f) { return (int)Math.Round(f); } + + // Returns the sign of /f/. + public static float Sign(float f) { return f >= 0F ? 1F : -1F; } + + // The infamous ''3.14159265358979...'' value (RO). + public const float PI = (float)Math.PI; + + // A representation of positive infinity (RO). + public const float Infinity = float.PositiveInfinity; + + // A representation of negative infinity (RO). + public const float NegativeInfinity = float.NegativeInfinity; + + // Degrees-to-radians conversion constant (RO). + public const float Deg2Rad = PI * 2F / 360F; + + // Radians-to-degrees conversion constant (RO). + public const float Rad2Deg = 1F / Deg2Rad; + + // Clamps a value between a minimum float and maximum float value. + public static double Clamp(double value, double min, double max) + { + if (value < min) + { + value = min; + } + else if (value > max) + { + value = max; + } + + return value; + } + + // Clamps a value between a minimum float and maximum float value. + public static float Clamp(float value, float min, float max) + { + if (value < min) + { + value = min; + } + else if (value > max) + { + value = max; + } + + return value; + } + + // Clamps value between min and max and returns value. + // Set the position of the transform to be that of the time + // but never less than 1 or more than 3 + // + public static int Clamp(int value, int min, int max) + { + if (value < min) + { + value = min; + } + else if (value > max) + { + value = max; + } + + return value; + } + + // Clamps value between 0 and 1 and returns value + public static float Clamp01(float value) + { + if (value < 0F) + { + return 0F; + } + else if (value > 1F) + { + return 1F; + } + else + { + return value; + } + } + + // Interpolates between /a/ and /b/ by /t/. /t/ is clamped between 0 and 1. + public static float Lerp(float a, float b, float t) + { + return a + (b - a) * Clamp01(t); + } + + // Interpolates between /a/ and /b/ by /t/ without clamping the interpolant. + public static float LerpUnclamped(float a, float b, float t) + { + return a + (b - a) * t; + } + + // Same as ::ref::Lerp but makes sure the values interpolate correctly when they wrap around 360 degrees. + public static float LerpAngle(float a, float b, float t) + { + float delta = Repeat((b - a), 360); + if (delta > 180) + { + delta -= 360; + } + + return a + delta * Clamp01(t); + } + + // Moves a value /current/ towards /target/. + public static float MoveTowards(float current, float target, float maxDelta) + { + if (MathF.Abs(target - current) <= maxDelta) + { + return target; + } + + return current + MathF.Sign(target - current) * maxDelta; + } + + // Same as ::ref::MoveTowards but makes sure the values interpolate correctly when they wrap around 360 degrees. + public static float MoveTowardsAngle(float current, float target, float maxDelta) + { + float deltaAngle = DeltaAngle(current, target); + if (-maxDelta < deltaAngle && deltaAngle < maxDelta) + { + return target; + } + + target = current + deltaAngle; + return MoveTowards(current, target, maxDelta); + } + + // Interpolates between /min/ and /max/ with smoothing at the limits. + public static float SmoothStep(float from, float to, float t) + { + t = MathF.Clamp01(t); + t = -2.0F * t * t * t + 3.0F * t * t; + return to * t + from * (1F - t); + } + + //*undocumented + public static float Gamma(float value, float absmax, float gamma) + { + bool negative = value < 0F; + float absval = Abs(value); + if (absval > absmax) + { + return negative ? -absval : absval; + } + + float result = Pow(absval / absmax, gamma) * absmax; + return negative ? -result : result; + } + + // Loops the value t, so that it is never larger than length and never smaller than 0. + public static float Repeat(float t, float length) + { + return Clamp(t - MathF.Floor(t / length) * length, 0.0f, length); + } + + // PingPongs the value t, so that it is never larger than length and never smaller than 0. + public static float PingPong(float t, float length) + { + t = Repeat(t, length * 2F); + return length - MathF.Abs(t - length); + } + + // Calculates the ::ref::Lerp parameter between of two values. + public static float InverseLerp(float a, float b, float value) + { + if (a != b) + { + return Clamp01((value - a) / (b - a)); + } + else + { + return 0.0f; + } + } + + // Calculates the shortest difference between two given angles. + public static float DeltaAngle(float current, float target) + { + float delta = MathF.Repeat((target - current), 360.0F); + if (delta > 180.0F) + { + delta -= 360.0F; + } + + return delta; + } + + internal static long RandomToLong(System.Random r) + { + byte[] buffer = new byte[8]; + r.NextBytes(buffer); + return (long)(System.BitConverter.ToUInt64(buffer, 0) & long.MaxValue); + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs b/LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs new file mode 100644 index 0000000..9c49ae0 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs @@ -0,0 +1,114 @@ +using System.Runtime.CompilerServices; + +namespace Nanomesh +{ + public static class MathUtils + { + public const float EpsilonFloat = 1e-15f; + public const double EpsilonDouble = 1e-40f; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DivideSafe(float numerator, float denominator) + { + return (denominator > -EpsilonFloat && denominator < EpsilonFloat) ? 0f : numerator / denominator; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double DivideSafe(double numerator, double denominator) + { + return (denominator > -EpsilonDouble && denominator < EpsilonDouble) ? 0d : numerator / denominator; + } + + public static void SelectMin(double e1, double e2, double e3, in T v1, in T v2, in T v3, out double e, out T v) + { + if (e1 < e2) + { + if (e1 < e3) + { + e = e1; + v = v1; + } + else + { + e = e3; + v = v3; + } + } + else + { + if (e2 < e3) + { + e = e2; + v = v2; + } + else + { + e = e3; + v = v3; + } + } + } + + public static void SelectMin(double e1, double e2, double e3, double e4, in T v1, in T v2, in T v3, in T v4, out double e, out T v) + { + if (e1 < e2) + { + if (e1 < e3) + { + if (e1 < e4) + { + e = e1; + v = v1; + } + else + { + e = e4; + v = v4; + } + } + else + { + if (e3 < e4) + { + e = e3; + v = v3; + } + else + { + e = e4; + v = v4; + } + } + } + else + { + if (e2 < e3) + { + if (e2 < e4) + { + e = e2; + v = v2; + } + else + { + e = e4; + v = v4; + } + } + else + { + if (e3 < e4) + { + e = e3; + v = v3; + } + else + { + e = e4; + v = v4; + } + } + } + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Profiling.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Profiling.cs new file mode 100644 index 0000000..d102493 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Profiling.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Nanomesh +{ + public static class Profiling + { + private static readonly Dictionary stopwatches = new Dictionary(); + + public static void Start(string key) + { + if (!stopwatches.ContainsKey(key)) + { + stopwatches.Add(key, Stopwatch.StartNew()); + } + else + { + stopwatches[key] = Stopwatch.StartNew(); + } + } + + public static string End(string key) + { + TimeSpan time = EndTimer(key); + return $"{key} done in {time.ToString("mm':'ss':'fff")}"; + } + + private static TimeSpan EndTimer(string key) + { + if (!stopwatches.ContainsKey(key)) + { + return TimeSpan.MinValue; + } + + Stopwatch sw = stopwatches[key]; + sw.Stop(); + stopwatches.Remove(key); + return sw.Elapsed; + } + + public static TimeSpan Time(Action toTime) + { + Stopwatch timer = Stopwatch.StartNew(); + toTime(); + timer.Stop(); + return timer.Elapsed; + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Quaternion.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Quaternion.cs new file mode 100644 index 0000000..d819f21 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Quaternion.cs @@ -0,0 +1,632 @@ +using System; +using System.Runtime.InteropServices; + +namespace Nanomesh +{ + [StructLayout(LayoutKind.Sequential)] + public partial struct Quaternion : IEquatable + { + private const double radToDeg = 180.0 / Math.PI; + private const double degToRad = Math.PI / 180.0; + + public const double kEpsilon = 1E-20; // should probably be used in the 0 tests in LookRotation or Slerp + + public Vector3 xyz + { + set + { + x = value.x; + y = value.y; + z = value.z; + } + get => new Vector3(x, y, z); + } + + public double x; + + public double y; + + public double z; + + public double w; + + public double this[int index] + { + get + { + switch (index) + { + case 0: + return x; + case 1: + return y; + case 2: + return z; + case 3: + return w; + default: + throw new IndexOutOfRangeException("Invalid Quaternion index: " + index + ", can use only 0,1,2,3"); + } + } + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + case 3: + w = value; + break; + default: + throw new IndexOutOfRangeException("Invalid Quaternion index: " + index + ", can use only 0,1,2,3"); + } + } + } + /// + /// The identity rotation (RO). + /// + public static Quaternion identity => new Quaternion(0, 0, 0, 1); + + /// + /// Gets the length (magnitude) of the quaternion. + /// + /// + public double Length => (double)System.Math.Sqrt(x * x + y * y + z * z + w * w); + + /// + /// Gets the square of the quaternion length (magnitude). + /// + public double LengthSquared => x * x + y * y + z * z + w * w; + + /// + /// Constructs new Quaternion with given x,y,z,w components. + /// + /// + /// + /// + /// + public Quaternion(double x, double y, double z, double w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + /// + /// Construct a new Quaternion from vector and w components + /// + /// The vector part + /// The w part + public Quaternion(Vector3 v, double w) + { + x = v.x; + y = v.y; + z = v.z; + this.w = w; + } + + /// + /// Set x, y, z and w components of an existing Quaternion. + /// + /// + /// + /// + /// + public void Set(double new_x, double new_y, double new_z, double new_w) + { + x = new_x; + y = new_y; + z = new_z; + w = new_w; + } + + /// + /// Scales the Quaternion to unit length. + /// + public static Quaternion Normalize(Quaternion q) + { + double mag = Math.Sqrt(Dot(q, q)); + + if (mag < kEpsilon) + { + return Quaternion.identity; + } + + return new Quaternion(q.x / mag, q.y / mag, q.z / mag, q.w / mag); + } + + /// + /// Scale the given quaternion to unit length + /// + /// The quaternion to normalize + /// The normalized quaternion + public void Normalize() + { + this = Normalize(this); + } + + /// + /// The dot product between two rotations. + /// + /// + /// + public static double Dot(Quaternion a, Quaternion b) + { + return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; + } + + /// + /// Creates a rotation which rotates /angle/ degrees around /axis/. + /// + /// + /// + public static Quaternion AngleAxis(double angle, Vector3 axis) + { + return Quaternion.AngleAxis(angle, ref axis); + } + + private static Quaternion AngleAxis(double degress, ref Vector3 axis) + { + if (axis.LengthSquared == 0.0) + { + return identity; + } + + Quaternion result = identity; + double radians = degress * degToRad; + radians *= 0.5; + axis = axis.Normalized; + axis = axis * Math.Sin(radians); + result.x = axis.x; + result.y = axis.y; + result.z = axis.z; + result.w = Math.Cos(radians); + + return Normalize(result); + } + + public void ToAngleAxis(out double angle, out Vector3 axis) + { + Quaternion.ToAxisAngleRad(this, out axis, out angle); + angle *= radToDeg; + } + + /// + /// Creates a rotation which rotates from /fromDirection/ to /toDirection/. + /// + /// + /// + public static Quaternion FromToRotation(Vector3 fromDirection, Vector3 toDirection) + { + return RotateTowards(LookRotation(fromDirection), LookRotation(toDirection), double.MaxValue); + } + + /// + /// Creates a rotation which rotates from /fromDirection/ to /toDirection/. + /// + /// + /// + public void SetFromToRotation(Vector3 fromDirection, Vector3 toDirection) + { + this = Quaternion.FromToRotation(fromDirection, toDirection); + } + + /// + /// Creates a rotation with the specified /forward/ and /upwards/ directions. + /// + /// The direction to look in. + /// The vector that defines in which direction up is. + public static Quaternion LookRotation(Vector3 forward, Vector3 upwards) + { + return Quaternion.LookRotation(ref forward, ref upwards); + } + + public static Quaternion LookRotation(Vector3 forward) + { + Vector3 up = new Vector3(1, 0, 0); + return Quaternion.LookRotation(ref forward, ref up); + } + + private static Quaternion LookRotation(ref Vector3 forward, ref Vector3 up) + { + forward = Vector3.Normalize(forward); + Vector3 right = Vector3.Normalize(Vector3.Cross(up, forward)); + up = Vector3.Cross(forward, right); + double m00 = right.x; + double m01 = right.y; + double m02 = right.z; + double m10 = up.x; + double m11 = up.y; + double m12 = up.z; + double m20 = forward.x; + double m21 = forward.y; + double m22 = forward.z; + + double num8 = (m00 + m11) + m22; + Quaternion quaternion = new Quaternion(); + if (num8 > 0) + { + double num = Math.Sqrt(num8 + 1); + quaternion.w = num * 0.5; + num = 0.5 / num; + quaternion.x = (m12 - m21) * num; + quaternion.y = (m20 - m02) * num; + quaternion.z = (m01 - m10) * num; + return quaternion; + } + if ((m00 >= m11) && (m00 >= m22)) + { + double num7 = Math.Sqrt(((1 + m00) - m11) - m22); + double num4 = 0.5 / num7; + quaternion.x = 0.5 * num7; + quaternion.y = (m01 + m10) * num4; + quaternion.z = (m02 + m20) * num4; + quaternion.w = (m12 - m21) * num4; + return quaternion; + } + if (m11 > m22) + { + double num6 = Math.Sqrt(((1 + m11) - m00) - m22); + double num3 = 0.5 / num6; + quaternion.x = (m10 + m01) * num3; + quaternion.y = 0.5 * num6; + quaternion.z = (m21 + m12) * num3; + quaternion.w = (m20 - m02) * num3; + return quaternion; + } + double num5 = Math.Sqrt(((1 + m22) - m00) - m11); + double num2 = 0.5 / num5; + quaternion.x = (m20 + m02) * num2; + quaternion.y = (m21 + m12) * num2; + quaternion.z = 0.5 * num5; + quaternion.w = (m01 - m10) * num2; + return quaternion; + } + + public void SetLookRotation(Vector3 view) + { + Vector3 up = new Vector3(1, 0, 0); + SetLookRotation(view, up); + } + + /// + /// Creates a rotation with the specified /forward/ and /upwards/ directions. + /// + /// The direction to look in. + /// The vector that defines in which direction up is. + public void SetLookRotation(Vector3 view, Vector3 up) + { + this = Quaternion.LookRotation(view, up); + } + + /// + /// Spherically interpolates between /a/ and /b/ by t. The parameter /t/ is clamped to the range [0, 1]. + /// + /// + /// + /// + public static Quaternion Slerp(Quaternion a, Quaternion b, double t) + { + return Quaternion.Slerp(ref a, ref b, t); + } + + private static Quaternion Slerp(ref Quaternion a, ref Quaternion b, double t) + { + if (t > 1) + { + t = 1; + } + + if (t < 0) + { + t = 0; + } + + return SlerpUnclamped(ref a, ref b, t); + } + + /// + /// Spherically interpolates between /a/ and /b/ by t. The parameter /t/ is not clamped. + /// + /// + /// + /// + public static Quaternion SlerpUnclamped(Quaternion a, Quaternion b, double t) + { + + return Quaternion.SlerpUnclamped(ref a, ref b, t); + } + private static Quaternion SlerpUnclamped(ref Quaternion a, ref Quaternion b, double t) + { + // if either input is zero, return the other. + if (a.LengthSquared == 0.0) + { + if (b.LengthSquared == 0.0) + { + return identity; + } + return b; + } + else if (b.LengthSquared == 0.0) + { + return a; + } + + double cosHalfAngle = a.w * b.w + Vector3.Dot(a.xyz, b.xyz); + + if (cosHalfAngle >= 1.0 || cosHalfAngle <= -1.0) + { + // angle = 0.0f, so just return one input. + return a; + } + else if (cosHalfAngle < 0.0) + { + b.xyz = -b.xyz; + b.w = -b.w; + cosHalfAngle = -cosHalfAngle; + } + + double blendA; + double blendB; + if (cosHalfAngle < 0.99) + { + // do proper slerp for big angles + double halfAngle = Math.Acos(cosHalfAngle); + double sinHalfAngle = Math.Sin(halfAngle); + double oneOverSinHalfAngle = 1.0 / sinHalfAngle; + blendA = Math.Sin(halfAngle * (1.0 - t)) * oneOverSinHalfAngle; + blendB = Math.Sin(halfAngle * t) * oneOverSinHalfAngle; + } + else + { + // do lerp if angle is really small. + blendA = 1.0f - t; + blendB = t; + } + + Quaternion result = new Quaternion(blendA * a.xyz + blendB * b.xyz, blendA * a.w + blendB * b.w); + if (result.LengthSquared > 0.0) + { + return Normalize(result); + } + else + { + return identity; + } + } + + /// + /// Interpolates between /a/ and /b/ by /t/ and normalizes the result afterwards. The parameter /t/ is clamped to the range [0, 1]. + /// + /// + /// + /// + public static Quaternion Lerp(Quaternion a, Quaternion b, double t) + { + if (t > 1) + { + t = 1; + } + + if (t < 0) + { + t = 0; + } + + return Slerp(ref a, ref b, t); // TODO: use lerp not slerp, "Because quaternion works in 4D. Rotation in 4D are linear" ??? + } + + /// + /// Interpolates between /a/ and /b/ by /t/ and normalizes the result afterwards. The parameter /t/ is not clamped. + /// + /// + /// + /// + public static Quaternion LerpUnclamped(Quaternion a, Quaternion b, double t) + { + return Slerp(ref a, ref b, t); + } + + /// + /// Rotates a rotation /from/ towards /to/. + /// + /// + /// + /// + public static Quaternion RotateTowards(Quaternion from, Quaternion to, double maxDegreesDelta) + { + double num = Quaternion.Angle(from, to); + if (num == 0) + { + return to; + } + double t = Math.Min(1, maxDegreesDelta / num); + return Quaternion.SlerpUnclamped(from, to, t); + } + + /// + /// Returns the Inverse of /rotation/. + /// + /// + public static Quaternion Inverse(Quaternion rotation) + { + double lengthSq = rotation.LengthSquared; + if (lengthSq != 0.0) + { + double i = 1.0 / lengthSq; + return new Quaternion(rotation.xyz * -i, rotation.w * i); + } + return rotation; + } + + /// + /// Returns a nicely formatted string of the Quaternion. + /// + /// + public override string ToString() + { + return $"{x}, {y}, {z}, {w}"; + } + + /// + /// Returns a nicely formatted string of the Quaternion. + /// + /// + public string ToString(string format) + { + return string.Format("({0}, {1}, {2}, {3})", x.ToString(format), y.ToString(format), z.ToString(format), w.ToString(format)); + } + + /// + /// Returns the angle in degrees between two rotations /a/ and /b/. + /// + /// + /// + public static double Angle(Quaternion a, Quaternion b) + { + double f = Quaternion.Dot(a, b); + return Math.Acos(Math.Min(Math.Abs(f), 1)) * 2 * radToDeg; + } + + /// + /// Returns a rotation that rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis (in that order). + /// + /// + /// + /// + public static Quaternion Euler(double x, double y, double z) + { + return Quaternion.FromEulerRad(new Vector3((double)x, (double)y, (double)z) * degToRad); + } + + /// + /// Returns a rotation that rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis (in that order). + /// + /// + public static Quaternion Euler(Vector3 euler) + { + return Quaternion.FromEulerRad(euler * degToRad); + } + + private static double NormalizeAngle(double angle) + { + while (angle > 360) + { + angle -= 360; + } + + while (angle < 0) + { + angle += 360; + } + + return angle; + } + + private static Quaternion FromEulerRad(Vector3 euler) + { + double yaw = euler.x; + double pitch = euler.y; + double roll = euler.z; + double rollOver2 = roll * 0.5; + double sinRollOver2 = (double)System.Math.Sin((double)rollOver2); + double cosRollOver2 = (double)System.Math.Cos((double)rollOver2); + double pitchOver2 = pitch * 0.5; + double sinPitchOver2 = (double)System.Math.Sin((double)pitchOver2); + double cosPitchOver2 = (double)System.Math.Cos((double)pitchOver2); + double yawOver2 = yaw * 0.5; + double sinYawOver2 = (double)System.Math.Sin((double)yawOver2); + double cosYawOver2 = (double)System.Math.Cos((double)yawOver2); + Quaternion result; + result.x = cosYawOver2 * cosPitchOver2 * cosRollOver2 + sinYawOver2 * sinPitchOver2 * sinRollOver2; + result.y = cosYawOver2 * cosPitchOver2 * sinRollOver2 - sinYawOver2 * sinPitchOver2 * cosRollOver2; + result.z = cosYawOver2 * sinPitchOver2 * cosRollOver2 + sinYawOver2 * cosPitchOver2 * sinRollOver2; + result.w = sinYawOver2 * cosPitchOver2 * cosRollOver2 - cosYawOver2 * sinPitchOver2 * sinRollOver2; + return result; + } + + private static void ToAxisAngleRad(Quaternion q, out Vector3 axis, out double angle) + { + if (System.Math.Abs(q.w) > 1.0) + { + q.Normalize(); + } + + angle = 2.0f * (double)System.Math.Acos(q.w); // angle + double den = (double)System.Math.Sqrt(1.0 - q.w * q.w); + if (den > 0.0001) + { + axis = q.xyz / den; + } + else + { + // This occurs when the angle is zero. + // Not a problem: just set an arbitrary normalized axis. + axis = new Vector3(1, 0, 0); + } + } + + public override int GetHashCode() + { + return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1; + } + public override bool Equals(object other) + { + if (!(other is Quaternion)) + { + return false; + } + Quaternion quaternion = (Quaternion)other; + return x.Equals(quaternion.x) && y.Equals(quaternion.y) && z.Equals(quaternion.z) && w.Equals(quaternion.w); + } + + public bool Equals(Quaternion other) + { + return x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z) && w.Equals(other.w); + } + + public static Quaternion operator *(Quaternion lhs, Quaternion rhs) + { + return new Quaternion(lhs.w * rhs.x + lhs.x * rhs.w + lhs.y * rhs.z - lhs.z * rhs.y, lhs.w * rhs.y + lhs.y * rhs.w + lhs.z * rhs.x - lhs.x * rhs.z, lhs.w * rhs.z + lhs.z * rhs.w + lhs.x * rhs.y - lhs.y * rhs.x, lhs.w * rhs.w - lhs.x * rhs.x - lhs.y * rhs.y - lhs.z * rhs.z); + } + + public static Vector3 operator *(Quaternion rotation, Vector3 point) + { + double num = rotation.x * 2; + double num2 = rotation.y * 2; + double num3 = rotation.z * 2; + double num4 = rotation.x * num; + double num5 = rotation.y * num2; + double num6 = rotation.z * num3; + double num7 = rotation.x * num2; + double num8 = rotation.x * num3; + double num9 = rotation.y * num3; + double num10 = rotation.w * num; + double num11 = rotation.w * num2; + double num12 = rotation.w * num3; + + return new Vector3( + (1 - (num5 + num6)) * point.x + (num7 - num12) * point.y + (num8 + num11) * point.z, + (num7 + num12) * point.x + (1 - (num4 + num6)) * point.y + (num9 - num10) * point.z, + (num8 - num11) * point.x + (num9 + num10) * point.y + (1 - (num4 + num5)) * point.z); + } + + public static bool operator ==(Quaternion lhs, Quaternion rhs) + { + return Quaternion.Dot(lhs, rhs) > 0.999999999; + } + + public static bool operator !=(Quaternion lhs, Quaternion rhs) + { + return Quaternion.Dot(lhs, rhs) <= 0.999999999; + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/SymmetricMatrix.cs b/LightlessSync/ThirdParty/Nanomesh/Base/SymmetricMatrix.cs new file mode 100644 index 0000000..d7be6a4 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/SymmetricMatrix.cs @@ -0,0 +1,97 @@ +namespace Nanomesh +{ + public readonly struct SymmetricMatrix + { + public readonly double m0, m1, m2, m3, m4, m5, m6, m7, m8, m9; + + public SymmetricMatrix(in double m0, in double m1, in double m2, in double m3, in double m4, in double m5, in double m6, in double m7, in double m8, in double m9) + { + this.m0 = m0; + this.m1 = m1; + this.m2 = m2; + this.m3 = m3; + this.m4 = m4; + this.m5 = m5; + this.m6 = m6; + this.m7 = m7; + this.m8 = m8; + this.m9 = m9; + } + + public SymmetricMatrix(in double a, in double b, in double c, in double d) + { + m0 = a * a; + m1 = a * b; + m2 = a * c; + m3 = a * d; + + m4 = b * b; + m5 = b * c; + m6 = b * d; + + m7 = c * c; + m8 = c * d; + + m9 = d * d; + } + + public static SymmetricMatrix operator +(in SymmetricMatrix a, in SymmetricMatrix b) + { + return new SymmetricMatrix( + a.m0 + b.m0, a.m1 + b.m1, a.m2 + b.m2, a.m3 + b.m3, + a.m4 + b.m4, a.m5 + b.m5, a.m6 + b.m6, + a.m7 + b.m7, a.m8 + b.m8, + a.m9 + b.m9 + ); + } + + public double DeterminantXYZ() + { + return + m0 * m4 * m7 + + m2 * m1 * m5 + + m1 * m5 * m2 - + m2 * m4 * m2 - + m0 * m5 * m5 - + m1 * m1 * m7; + } + + public double DeterminantX() + { + return + m1 * m5 * m8 + + m3 * m4 * m7 + + m2 * m6 * m5 - + m3 * m5 * m5 - + m1 * m6 * m7 - + m2 * m4 * m8; + } + + public double DeterminantY() + { + return + m0 * m5 * m8 + + m3 * m1 * m7 + + m2 * m6 * m2 - + m3 * m5 * m2 - + m0 * m6 * m7 - + m2 * m1 * m8; + } + + public double DeterminantZ() + { + return + m0 * m4 * m8 + + m3 * m1 * m5 + + m1 * m6 * m2 - + m3 * m4 * m2 - + m0 * m6 * m5 - + m1 * m1 * m8; + } + + public override string ToString() + { + return $"{m0} {m1} {m2} {m3}| {m4} {m5} {m6} | {m7} {m8} | {m9}"; + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/TextUtils.cs b/LightlessSync/ThirdParty/Nanomesh/Base/TextUtils.cs new file mode 100644 index 0000000..6669eec --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/TextUtils.cs @@ -0,0 +1,26 @@ +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace Nanomesh +{ + public static class TextUtils + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double ToDouble(this string text) + { + return double.Parse(text, CultureInfo.InvariantCulture); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ToFloat(this string text) + { + return float.Parse(text, CultureInfo.InvariantCulture); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ToInt(this string text) + { + return int.Parse(text, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector2.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector2.cs new file mode 100644 index 0000000..484d8ba --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector2.cs @@ -0,0 +1,377 @@ +using System; + +namespace Nanomesh +{ + public readonly struct Vector2 : IEquatable, IInterpolable + { + public readonly double x; + public readonly double y; + + // Access the /x/ or /y/ component using [0] or [1] respectively. + public double this[int index] + { + get + { + switch (index) + { + case 0: return x; + case 1: return y; + default: + throw new IndexOutOfRangeException("Invalid Vector2 index!"); + } + } + } + + // Constructs a new vector with given x, y components. + public Vector2(double x, double y) { this.x = x; this.y = y; } + + // Linearly interpolates between two vectors. + public static Vector2 Lerp(Vector2 a, Vector2 b, double t) + { + t = MathF.Clamp(t, 0, 1); + return new Vector2( + a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t + ); + } + + // Linearly interpolates between two vectors without clamping the interpolant + public static Vector2 LerpUnclamped(Vector2 a, Vector2 b, double t) + { + return new Vector2( + a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t + ); + } + + // Moves a point /current/ towards /target/. + public static Vector2 MoveTowards(Vector2 current, Vector2 target, double maxDistanceDelta) + { + // avoid vector ops because current scripting backends are terrible at inlining + double toVector_x = target.x - current.x; + double toVector_y = target.y - current.y; + + double sqDist = toVector_x * toVector_x + toVector_y * toVector_y; + + if (sqDist == 0 || (maxDistanceDelta >= 0 && sqDist <= maxDistanceDelta * maxDistanceDelta)) + { + return target; + } + + double dist = Math.Sqrt(sqDist); + + return new Vector2(current.x + toVector_x / dist * maxDistanceDelta, + current.y + toVector_y / dist * maxDistanceDelta); + } + + // Multiplies two vectors component-wise. + public static Vector2 Scale(Vector2 a, Vector2 b) => new Vector2(a.x * b.x, a.y * b.y); + + public static Vector2 Normalize(in Vector2 value) + { + double mag = Magnitude(in value); + if (mag > K_EPSILON) + { + return value / mag; + } + else + { + return Zero; + } + } + + public Vector2 Normalize() => Normalize(in this); + + public static double SqrMagnitude(in Vector2 a) => a.x * a.x + a.y * a.y; + + /// + /// Returns the squared length of this vector (RO). + /// + public double SqrMagnitude() => SqrMagnitude(in this); + + public static double Magnitude(in Vector2 vector) => Math.Sqrt(SqrMagnitude(in vector)); + + public double Magnitude() => Magnitude(this); + + // used to allow Vector2s to be used as keys in hash tables + public override int GetHashCode() + { + return x.GetHashCode() ^ (y.GetHashCode() << 2); + } + + // also required for being able to use Vector2s as keys in hash tables + public override bool Equals(object other) + { + if (!(other is Vector2)) + { + return false; + } + + return Equals((Vector2)other); + } + + + public bool Equals(Vector2 other) + { + return x == other.x && y == other.y; + } + + public static Vector2 Reflect(Vector2 inDirection, Vector2 inNormal) + { + double factor = -2F * Dot(inNormal, inDirection); + return new Vector2(factor * inNormal.x + inDirection.x, factor * inNormal.y + inDirection.y); + } + + + public static Vector2 Perpendicular(Vector2 inDirection) + { + return new Vector2(-inDirection.y, inDirection.x); + } + + /// + /// Returns the dot Product of two vectors. + /// + /// + /// + /// + public static double Dot(Vector2 lhs, Vector2 rhs) { return lhs.x * rhs.x + lhs.y * rhs.y; } + + /// + /// Returns the angle in radians between /from/ and /to/. + /// + /// + /// + /// + public static double AngleRadians(Vector2 from, Vector2 to) + { + // sqrt(a) * sqrt(b) = sqrt(a * b) -- valid for real numbers + double denominator = Math.Sqrt(from.SqrMagnitude() * to.SqrMagnitude()); + if (denominator < K_EPSILON_NORMAL_SQRT) + { + return 0F; + } + + double dot = MathF.Clamp(Dot(from, to) / denominator, -1F, 1F); + return Math.Acos(dot); + } + + public static double AngleDegrees(Vector2 from, Vector2 to) + { + return AngleRadians(from, to) / MathF.PI * 180f; + } + + /// + /// Returns the signed angle in degrees between /from/ and /to/. Always returns the smallest possible angle + /// + /// + /// + /// + public static double SignedAngle(Vector2 from, Vector2 to) + { + double unsigned_angle = AngleDegrees(from, to); + double sign = Math.Sign(from.x * to.y - from.y * to.x); + return unsigned_angle * sign; + } + + /// + /// Returns the distance between /a/ and /b/. + /// + /// + /// + /// + public static double Distance(Vector2 a, Vector2 b) + { + double diff_x = a.x - b.x; + double diff_y = a.y - b.y; + return Math.Sqrt(diff_x * diff_x + diff_y * diff_y); + } + + /// + /// Returns a copy of /vector/ with its magnitude clamped to /maxLength/. + /// + /// + /// + /// + public static Vector2 ClampMagnitude(Vector2 vector, double maxLength) + { + double sqrMagnitude = vector.SqrMagnitude(); + if (sqrMagnitude > maxLength * maxLength) + { + double mag = Math.Sqrt(sqrMagnitude); + + //these intermediate variables force the intermediate result to be + //of double precision. without this, the intermediate result can be of higher + //precision, which changes behavior. + double normalized_x = vector.x / mag; + double normalized_y = vector.y / mag; + return new Vector2(normalized_x * maxLength, + normalized_y * maxLength); + } + return vector; + } + + /// + /// Returns a vector that is made from the smallest components of two vectors. + /// + /// + /// + /// + public static Vector2 Min(Vector2 lhs, Vector2 rhs) { return new Vector2(Math.Min(lhs.x, rhs.x), Math.Min(lhs.y, rhs.y)); } + + /// + /// Returns a vector that is made from the largest components of two vectors. + /// + /// + /// + /// + public static Vector2 Max(Vector2 lhs, Vector2 rhs) { return new Vector2(Math.Max(lhs.x, rhs.x), Math.Max(lhs.y, rhs.y)); } + + public Vector2 Interpolate(Vector2 other, double ratio) => this * ratio + other * (1 - ratio); + + /// + /// Adds two vectors. + /// + /// + /// + /// + public static Vector2 operator +(Vector2 a, Vector2 b) { return new Vector2(a.x + b.x, a.y + b.y); } + + /// + /// Subtracts one vector from another. + /// + /// + /// + /// + public static Vector2 operator -(Vector2 a, Vector2 b) { return new Vector2(a.x - b.x, a.y - b.y); } + + /// + /// Multiplies one vector by another. + /// + /// + /// + /// + public static Vector2 operator *(Vector2 a, Vector2 b) { return new Vector2(a.x * b.x, a.y * b.y); } + + /// + /// Divides one vector over another. + /// + /// + /// + /// + public static Vector2 operator /(Vector2 a, Vector2 b) { return new Vector2(a.x / b.x, a.y / b.y); } + + /// + /// Negates a vector. + /// + /// + /// + public static Vector2 operator -(Vector2 a) { return new Vector2(-a.x, -a.y); } + + /// + /// Multiplies a vector by a number. + /// + /// + /// + /// + public static Vector2 operator *(Vector2 a, double d) { return new Vector2(a.x * d, a.y * d); } + + /// + /// Multiplies a vector by a number. + /// + /// + /// + /// + public static Vector2 operator *(double d, Vector2 a) { return new Vector2(a.x * d, a.y * d); } + + /// + /// Divides a vector by a number. + /// + /// + /// + /// + public static Vector2 operator /(Vector2 a, double d) { return new Vector2(a.x / d, a.y / d); } + + /// + /// Returns true if the vectors are equal. + /// + /// + /// + /// + public static bool operator ==(Vector2 lhs, Vector2 rhs) + { + // Returns false in the presence of NaN values. + double diff_x = lhs.x - rhs.x; + double diff_y = lhs.y - rhs.y; + return (diff_x * diff_x + diff_y * diff_y) < K_EPSILON * K_EPSILON; + } + + /// + /// Returns true if vectors are different. + /// + /// + /// + /// + public static bool operator !=(Vector2 lhs, Vector2 rhs) + { + // Returns true in the presence of NaN values. + return !(lhs == rhs); + } + + /// + /// Converts a [[Vector3]] to a Vector2. + /// + /// + public static implicit operator Vector2(Vector3F v) + { + return new Vector2(v.x, v.y); + } + + /// + /// Converts a Vector2 to a [[Vector3]]. + /// + /// + public static implicit operator Vector3(Vector2 v) + { + return new Vector3(v.x, v.y, 0); + } + + public static implicit operator Vector2F(Vector2 vec) + { + return new Vector2F((float)vec.x, (float)vec.y); + } + + public static explicit operator Vector2(Vector2F vec) + { + return new Vector2(vec.x, vec.y); + } + + public static readonly Vector2 zeroVector = new Vector2(0F, 0F); + public static readonly Vector2 oneVector = new Vector2(1F, 1F); + public static readonly Vector2 upVector = new Vector2(0F, 1F); + public static readonly Vector2 downVector = new Vector2(0F, -1F); + public static readonly Vector2 leftVector = new Vector2(-1F, 0F); + public static readonly Vector2 rightVector = new Vector2(1F, 0F); + public static readonly Vector2 positiveInfinityVector = new Vector2(double.PositiveInfinity, double.PositiveInfinity); + public static readonly Vector2 negativeInfinityVector = new Vector2(double.NegativeInfinity, double.NegativeInfinity); + + public static Vector2 Zero => zeroVector; + + public static Vector2 One => oneVector; + + public static Vector2 Up => upVector; + + public static Vector2 Down => downVector; + + public static Vector2 Left => leftVector; + + public static Vector2 Right => rightVector; + + public static Vector2 PositiveInfinity => positiveInfinityVector; + + public static Vector2 NegativeInfinity => negativeInfinityVector; + + public const double K_EPSILON = 0.00001F; + + public const double K_EPSILON_NORMAL_SQRT = 1e-15f; + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector2F.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector2F.cs new file mode 100644 index 0000000..9f3faa5 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector2F.cs @@ -0,0 +1,371 @@ +using System; + +namespace Nanomesh +{ + public readonly struct Vector2F : IEquatable, IInterpolable + { + public readonly float x; + public readonly float y; + + // Access the /x/ or /y/ component using [0] or [1] respectively. + public float this[int index] + { + get + { + switch (index) + { + case 0: return x; + case 1: return y; + default: + throw new IndexOutOfRangeException("Invalid Vector2 index!"); + } + } + } + + // Constructs a new vector with given x, y components. + public Vector2F(float x, float y) { this.x = x; this.y = y; } + + // Linearly interpolates between two vectors. + public static Vector2F Lerp(Vector2F a, Vector2F b, float t) + { + t = MathF.Clamp(t, 0, 1); + return new Vector2F( + a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t + ); + } + + // Linearly interpolates between two vectors without clamping the interpolant + public static Vector2F LerpUnclamped(Vector2F a, Vector2F b, float t) + { + return new Vector2F( + a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t + ); + } + + // Moves a point /current/ towards /target/. + public static Vector2F MoveTowards(Vector2F current, Vector2F target, float maxDistanceDelta) + { + // avoid vector ops because current scripting backends are terrible at inlining + float toVector_x = target.x - current.x; + float toVector_y = target.y - current.y; + + float sqDist = toVector_x * toVector_x + toVector_y * toVector_y; + + if (sqDist == 0 || (maxDistanceDelta >= 0 && sqDist <= maxDistanceDelta * maxDistanceDelta)) + { + return target; + } + + float dist = MathF.Sqrt(sqDist); + + return new Vector2F(current.x + toVector_x / dist * maxDistanceDelta, + current.y + toVector_y / dist * maxDistanceDelta); + } + + // Multiplies two vectors component-wise. + public static Vector2F Scale(Vector2F a, Vector2F b) { return new Vector2F(a.x * b.x, a.y * b.y); } + + public static Vector2F Normalize(in Vector2F value) + { + float mag = Magnitude(in value); + if (mag > K_EPSILON) + { + return value / mag; + } + else + { + return Zero; + } + } + + public Vector2F Normalize() => Normalize(in this); + + public static float SqrMagnitude(in Vector2F a) => a.x * a.x + a.y * a.y; + + /// + /// Returns the squared length of this vector (RO). + /// + public float SqrMagnitude() => SqrMagnitude(in this); + + public static float Magnitude(in Vector2F vector) => (float)Math.Sqrt(SqrMagnitude(in vector)); + + public float Magnitude() => Magnitude(this); + + // used to allow Vector2s to be used as keys in hash tables + public override int GetHashCode() + { + return x.GetHashCode() ^ (y.GetHashCode() << 2); + } + + // also required for being able to use Vector2s as keys in hash tables + public override bool Equals(object other) + { + if (!(other is Vector2F)) + { + return false; + } + + return Equals((Vector2F)other); + } + + + public bool Equals(Vector2F other) + { + return Vector2FComparer.Default.Equals(this, other); + //return x == other.x && y == other.y; + } + + public static Vector2F Reflect(Vector2F inDirection, Vector2F inNormal) + { + float factor = -2F * Dot(inNormal, inDirection); + return new Vector2F(factor * inNormal.x + inDirection.x, factor * inNormal.y + inDirection.y); + } + + public static Vector2F Perpendicular(Vector2F inDirection) + { + return new Vector2F(-inDirection.y, inDirection.x); + } + + /// + /// Returns the dot Product of two vectors. + /// + /// + /// + /// + public static float Dot(Vector2F lhs, Vector2F rhs) { return lhs.x * rhs.x + lhs.y * rhs.y; } + + /// + /// Returns the angle in radians between /from/ and /to/. + /// + /// + /// + /// + public static float AngleRadians(Vector2F from, Vector2F to) + { + // sqrt(a) * sqrt(b) = sqrt(a * b) -- valid for real numbers + float denominator = MathF.Sqrt(from.SqrMagnitude() * to.SqrMagnitude()); + if (denominator < K_EPSILON_NORMAL_SQRT) + { + return 0F; + } + + float dot = MathF.Clamp(Dot(from, to) / denominator, -1F, 1F); + return MathF.Acos(dot); + } + + public static float AngleDegrees(Vector2F from, Vector2F to) + { + return AngleRadians(from, to) / MathF.PI * 180f; + } + + /// + /// Returns the signed angle in degrees between /from/ and /to/. Always returns the smallest possible angle + /// + /// + /// + /// + public static float SignedAngle(Vector2F from, Vector2F to) + { + float unsigned_angle = AngleDegrees(from, to); + float sign = MathF.Sign(from.x * to.y - from.y * to.x); + return unsigned_angle * sign; + } + + /// + /// Returns the distance between /a/ and /b/. + /// + /// + /// + /// + public static float Distance(Vector2F a, Vector2F b) + { + float diff_x = a.x - b.x; + float diff_y = a.y - b.y; + return MathF.Sqrt(diff_x * diff_x + diff_y * diff_y); + } + + /// + /// Returns a copy of /vector/ with its magnitude clamped to /maxLength/. + /// + /// + /// + /// + public static Vector2F ClampMagnitude(Vector2F vector, float maxLength) + { + float sqrMagnitude = vector.SqrMagnitude(); + if (sqrMagnitude > maxLength * maxLength) + { + float mag = MathF.Sqrt(sqrMagnitude); + + //these intermediate variables force the intermediate result to be + //of float precision. without this, the intermediate result can be of higher + //precision, which changes behavior. + float normalized_x = vector.x / mag; + float normalized_y = vector.y / mag; + return new Vector2F(normalized_x * maxLength, + normalized_y * maxLength); + } + return vector; + } + + /// + /// Returns a vector that is made from the smallest components of two vectors. + /// + /// + /// + /// + public static Vector2F Min(Vector2F lhs, Vector2F rhs) { return new Vector2F(MathF.Min(lhs.x, rhs.x), MathF.Min(lhs.y, rhs.y)); } + + /// + /// Returns a vector that is made from the largest components of two vectors. + /// + /// + /// + /// + public static Vector2F Max(Vector2F lhs, Vector2F rhs) { return new Vector2F(MathF.Max(lhs.x, rhs.x), MathF.Max(lhs.y, rhs.y)); } + + public Vector2F Interpolate(Vector2F other, double ratio) => this * ratio + other * (1 - ratio); + + /// + /// Adds two vectors. + /// + /// + /// + /// + public static Vector2F operator +(Vector2F a, Vector2F b) { return new Vector2F(a.x + b.x, a.y + b.y); } + + /// + /// Subtracts one vector from another. + /// + /// + /// + /// + public static Vector2F operator -(Vector2F a, Vector2F b) { return new Vector2F(a.x - b.x, a.y - b.y); } + + /// + /// Multiplies one vector by another. + /// + /// + /// + /// + public static Vector2F operator *(Vector2F a, Vector2F b) { return new Vector2F(a.x * b.x, a.y * b.y); } + + /// + /// Divides one vector over another. + /// + /// + /// + /// + public static Vector2F operator /(Vector2F a, Vector2F b) { return new Vector2F(a.x / b.x, a.y / b.y); } + + /// + /// Negates a vector. + /// + /// + /// + public static Vector2F operator -(Vector2F a) { return new Vector2F(-a.x, -a.y); } + + /// + /// Multiplies a vector by a number. + /// + /// + /// + /// + public static Vector2F operator *(Vector2F a, float d) { return new Vector2F(a.x * d, a.y * d); } + + public static Vector2 operator *(Vector2F a, double d) { return new Vector2(a.x * d, a.y * d); } + + /// + /// Multiplies a vector by a number. + /// + /// + /// + /// + public static Vector2F operator *(float d, Vector2F a) { return new Vector2F(a.x * d, a.y * d); } + + public static Vector2 operator *(double d, Vector2F a) { return new Vector2(a.x * d, a.y * d); } + + /// + /// Divides a vector by a number. + /// + /// + /// + /// + public static Vector2F operator /(Vector2F a, float d) { return new Vector2F(a.x / d, a.y / d); } + + /// + /// Returns true if the vectors are equal. + /// + /// + /// + /// + public static bool operator ==(Vector2F lhs, Vector2F rhs) + { + // Returns false in the presence of NaN values. + float diff_x = lhs.x - rhs.x; + float diff_y = lhs.y - rhs.y; + return (diff_x * diff_x + diff_y * diff_y) < K_EPSILON * K_EPSILON; + } + + /// + /// Returns true if vectors are different. + /// + /// + /// + /// + public static bool operator !=(Vector2F lhs, Vector2F rhs) + { + // Returns true in the presence of NaN values. + return !(lhs == rhs); + } + + /// + /// Converts a [[Vector3]] to a Vector2. + /// + /// + public static implicit operator Vector2F(Vector3F v) + { + return new Vector2F(v.x, v.y); + } + + /// + /// Converts a Vector2 to a [[Vector3]]. + /// + /// + public static implicit operator Vector3(Vector2F v) + { + return new Vector3(v.x, v.y, 0); + } + + public static readonly Vector2F zeroVector = new Vector2F(0F, 0F); + public static readonly Vector2F oneVector = new Vector2F(1F, 1F); + public static readonly Vector2F upVector = new Vector2F(0F, 1F); + public static readonly Vector2F downVector = new Vector2F(0F, -1F); + public static readonly Vector2F leftVector = new Vector2F(-1F, 0F); + public static readonly Vector2F rightVector = new Vector2F(1F, 0F); + public static readonly Vector2F positiveInfinityVector = new Vector2F(float.PositiveInfinity, float.PositiveInfinity); + public static readonly Vector2F negativeInfinityVector = new Vector2F(float.NegativeInfinity, float.NegativeInfinity); + + public static Vector2F Zero => zeroVector; + + public static Vector2F One => oneVector; + + public static Vector2F Up => upVector; + + public static Vector2F Down => downVector; + + public static Vector2F Left => leftVector; + + public static Vector2F Right => rightVector; + + public static Vector2F PositiveInfinity => positiveInfinityVector; + + public static Vector2F NegativeInfinity => negativeInfinityVector; + + public const float K_EPSILON = 0.00001F; + + public const float K_EPSILON_NORMAL_SQRT = 1e-15f; + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector2FComparer.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector2FComparer.cs new file mode 100644 index 0000000..2519aaf --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector2FComparer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Nanomesh +{ + public class Vector2FComparer : IEqualityComparer + { + private static Vector2FComparer _instance; + public static Vector2FComparer Default => _instance ?? (_instance = new Vector2FComparer(0.0001f)); + + private readonly float _tolerance; + + public Vector2FComparer(float tolerance) + { + _tolerance = tolerance; + } + + public bool Equals(Vector2F x, Vector2F y) + { + return (int)(x.x / _tolerance) == (int)(y.x / _tolerance) + && (int)(x.y / _tolerance) == (int)(y.y / _tolerance); + } + + public int GetHashCode(Vector2F obj) + { + return (int)(obj.x / _tolerance) ^ ((int)(obj.y / _tolerance) << 2); + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector3.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3.cs new file mode 100644 index 0000000..96f79f9 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3.cs @@ -0,0 +1,191 @@ +using System; + +namespace Nanomesh +{ + public readonly struct Vector3 : IEquatable, IInterpolable + { + public readonly double x; + public readonly double y; + public readonly double z; + + public Vector3(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3(double x, double y) + { + this.x = x; + this.y = y; + z = 0.0; + } + + public double this[int index] + { + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + default: + throw new IndexOutOfRangeException("Invalid Vector3 index!"); + } + } + } + + public override int GetHashCode() + { + return x.GetHashCode() ^ (y.GetHashCode() << 2) ^ (z.GetHashCode() >> 2); + } + + public override bool Equals(object other) + { + if (!(other is Vector3)) + { + return false; + } + + return Equals((Vector3)other); + } + + public bool Equals(Vector3 other) + { + return x == other.x && y == other.y && z == other.z; + } + + public static Vector3 operator +(in Vector3 a, in Vector3 b) { return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z); } + + public static Vector3 operator -(in Vector3 a, in Vector3 b) { return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z); } + + public static Vector3 operator -(in Vector3 a) { return new Vector3(-a.x, -a.y, -a.z); } + + public static Vector3 operator *(in Vector3 a, double d) { return new Vector3(a.x * d, a.y * d, a.z * d); } + + public static Vector3 operator *(double d, in Vector3 a) { return new Vector3(a.x * d, a.y * d, a.z * d); } + + public static Vector3 operator /(in Vector3 a, double d) { return new Vector3(MathUtils.DivideSafe(a.x, d), MathUtils.DivideSafe(a.y, d), MathUtils.DivideSafe(a.z, d)); } + + public static bool operator ==(in Vector3 lhs, in Vector3 rhs) + { + double diff_x = lhs.x - rhs.x; + double diff_y = lhs.y - rhs.y; + double diff_z = lhs.z - rhs.z; + double sqrmag = diff_x * diff_x + diff_y * diff_y + diff_z * diff_z; + return sqrmag < MathUtils.EpsilonDouble; + } + + public static bool operator !=(in Vector3 lhs, in Vector3 rhs) + { + return !(lhs == rhs); + } + public static Vector3 Cross(in Vector3 lhs, in Vector3 rhs) + { + return new Vector3( + lhs.y * rhs.z - lhs.z * rhs.y, + lhs.z * rhs.x - lhs.x * rhs.z, + lhs.x * rhs.y - lhs.y * rhs.x); + } + + public static implicit operator Vector3F(Vector3 vec) + { + return new Vector3F((float)vec.x, (float)vec.y, (float)vec.z); + } + + public static explicit operator Vector3(Vector3F vec) + { + return new Vector3(vec.x, vec.y, vec.z); + } + + public static double Dot(in Vector3 lhs, in Vector3 rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; + } + + public static Vector3 Normalize(in Vector3 value) + { + double mag = Magnitude(value); + return value / mag; + } + + public Vector3 Normalized => Vector3.Normalize(this); + + public static double Distance(in Vector3 a, in Vector3 b) + { + double diff_x = a.x - b.x; + double diff_y = a.y - b.y; + double diff_z = a.z - b.z; + return Math.Sqrt(diff_x * diff_x + diff_y * diff_y + diff_z * diff_z); + } + + public static double Magnitude(in Vector3 vector) + { + return Math.Sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z); + } + + public static Vector3 ProjectPointOnLine(in Vector3 linePoint, in Vector3 lineVec, in Vector3 point) + { + Vector3 linePointToPoint = point - linePoint; + return linePoint + lineVec * Dot(linePointToPoint, lineVec); + } + + public static double DistancePointLine(in Vector3 point, in Vector3 lineStart, in Vector3 lineEnd) + { + return Magnitude(ProjectPointOnLine(lineStart, (lineEnd - lineStart).Normalized, point) - point); + } + + public double LengthSquared => x * x + y * y + z * z; + + public double Length => Math.Sqrt(x * x + y * y + z * z); + + public static Vector3 Min(in Vector3 lhs, in Vector3 rhs) + { + return new Vector3(Math.Min(lhs.x, rhs.x), Math.Min(lhs.y, rhs.y), Math.Min(lhs.z, rhs.z)); + } + + public static Vector3 Max(in Vector3 lhs, in Vector3 rhs) + { + return new Vector3(Math.Max(lhs.x, rhs.x), Math.Max(lhs.y, rhs.y), Math.Max(lhs.z, rhs.z)); + } + + public static readonly Vector3 zeroVector = new Vector3(0f, 0f, 0f); + public static readonly Vector3 oneVector = new Vector3(1f, 1f, 1f); + public static readonly Vector3 positiveInfinityVector = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + public static readonly Vector3 negativeInfinityVector = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + + public static Vector3 Zero => zeroVector; + + public static Vector3 One => oneVector; + + public static Vector3 PositiveInfinity => positiveInfinityVector; + + public static Vector3 NegativeInfinity => negativeInfinityVector; + + public static double AngleRadians(in Vector3 from, in Vector3 to) + { + double denominator = Math.Sqrt(from.LengthSquared * to.LengthSquared); + if (denominator < 1e-15F) + { + return 0F; + } + + double dot = MathF.Clamp(Dot(from, to) / denominator, -1.0, 1.0); + return Math.Acos(dot); + } + + public static double AngleDegrees(in Vector3 from, in Vector3 to) + { + return AngleRadians(from, to) / Math.PI * 180d; + } + + public override string ToString() + { + return $"{x}, {y}, {z}"; + } + + public Vector3 Interpolate(Vector3 other, double ratio) => this * ratio + other * (1 - ratio); + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector3Comparer.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3Comparer.cs new file mode 100644 index 0000000..9dbf2fb --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3Comparer.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Nanomesh +{ + public class Vector3Comparer : IEqualityComparer + { + private readonly double _tolerance; + + public Vector3Comparer(double tolerance) + { + _tolerance = tolerance; + } + + public bool Equals(Vector3 x, Vector3 y) + { + return (int)(x.x / _tolerance) == (int)(y.x / _tolerance) + && (int)(x.y / _tolerance) == (int)(y.y / _tolerance) + && (int)(x.z / _tolerance) == (int)(y.z / _tolerance); + } + + public int GetHashCode(Vector3 obj) + { + return (int)(obj.x / _tolerance) ^ ((int)(obj.y / _tolerance) << 2) ^ ((int)(obj.z / _tolerance) >> 2); + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector3F.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3F.cs new file mode 100644 index 0000000..57b92bf --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3F.cs @@ -0,0 +1,172 @@ +using System; + +namespace Nanomesh +{ + public readonly struct Vector3F : IEquatable, IInterpolable + { + public readonly float x; + public readonly float y; + public readonly float z; + + public Vector3F(float x, float y, float z) + { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3F(float x, float y) + { + this.x = x; + this.y = y; + z = 0F; + } + + public float this[int index] + { + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + default: + throw new IndexOutOfRangeException("Invalid Vector3F index!"); + } + } + } + + public override int GetHashCode() + { + return Vector3FComparer.Default.GetHashCode(this); + //return x.GetHashCode() ^ (y.GetHashCode() << 2) ^ (z.GetHashCode() >> 2); + } + + public override bool Equals(object other) + { + if (!(other is Vector3F)) + { + return false; + } + + return Equals((Vector3F)other); + } + + public bool Equals(Vector3F other) + { + return Vector3FComparer.Default.Equals(this, other); + //return x == other.x && y == other.y && z == other.z; + } + + public static Vector3F operator +(in Vector3F a, in Vector3F b) { return new Vector3F(a.x + b.x, a.y + b.y, a.z + b.z); } + + public static Vector3F operator -(in Vector3F a, in Vector3F b) { return new Vector3F(a.x - b.x, a.y - b.y, a.z - b.z); } + + public static Vector3F operator -(in Vector3F a) { return new Vector3F(-a.x, -a.y, -a.z); } + + public static Vector3F operator *(in Vector3F a, float d) { return new Vector3F(a.x * d, a.y * d, a.z * d); } + + public static Vector3F operator *(float d, in Vector3F a) { return new Vector3F(a.x * d, a.y * d, a.z * d); } + + public static Vector3 operator *(double d, in Vector3F a) { return new Vector3(a.x * d, a.y * d, a.z * d); } + + public static Vector3F operator /(in Vector3F a, float d) { return new Vector3F(MathUtils.DivideSafe(a.x, d), MathUtils.DivideSafe(a.y, d), MathUtils.DivideSafe(a.z, d)); } + + public static bool operator ==(in Vector3F lhs, in Vector3F rhs) + { + float diff_x = lhs.x - rhs.x; + float diff_y = lhs.y - rhs.y; + float diff_z = lhs.z - rhs.z; + float sqrmag = diff_x * diff_x + diff_y * diff_y + diff_z * diff_z; + return sqrmag < MathUtils.EpsilonFloat; + } + + public static bool operator !=(in Vector3F lhs, in Vector3F rhs) + { + return !(lhs == rhs); + } + public static Vector3F Cross(in Vector3F lhs, in Vector3F rhs) + { + return new Vector3F( + lhs.y * rhs.z - lhs.z * rhs.y, + lhs.z * rhs.x - lhs.x * rhs.z, + lhs.x * rhs.y - lhs.y * rhs.x); + } + + public static float Dot(in Vector3F lhs, in Vector3F rhs) + { + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; + } + + public static Vector3F Normalize(in Vector3F value) + { + float mag = Magnitude(value); + return value / mag; + } + + public Vector3F Normalized => Vector3F.Normalize(this); + + public static float Distance(in Vector3F a, in Vector3F b) + { + float diff_x = a.x - b.x; + float diff_y = a.y - b.y; + float diff_z = a.z - b.z; + return MathF.Sqrt(diff_x * diff_x + diff_y * diff_y + diff_z * diff_z); + } + + public static float Magnitude(in Vector3F vector) + { + return MathF.Sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z); + } + + public float SqrMagnitude => x * x + y * y + z * z; + + public static Vector3F Min(in Vector3F lhs, in Vector3F rhs) + { + return new Vector3F(MathF.Min(lhs.x, rhs.x), MathF.Min(lhs.y, rhs.y), MathF.Min(lhs.z, rhs.z)); + } + + public static Vector3F Max(in Vector3F lhs, in Vector3F rhs) + { + return new Vector3F(MathF.Max(lhs.x, rhs.x), MathF.Max(lhs.y, rhs.y), MathF.Max(lhs.z, rhs.z)); + } + + public static readonly Vector3F zeroVector = new Vector3F(0f, 0f, 0f); + public static readonly Vector3F oneVector = new Vector3F(1f, 1f, 1f); + public static readonly Vector3F positiveInfinityVector = new Vector3F(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + public static readonly Vector3F negativeInfinityVector = new Vector3F(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + + public static Vector3F Zero => zeroVector; + + public static Vector3F One => oneVector; + + public static Vector3F PositiveInfinity => positiveInfinityVector; + + public static Vector3F NegativeInfinity => negativeInfinityVector; + + public static float AngleRadians(in Vector3F from, in Vector3F to) + { + float denominator = MathF.Sqrt(from.SqrMagnitude * to.SqrMagnitude); + if (denominator < 1e-15F) + { + return 0F; + } + + float dot = MathF.Clamp(Dot(from, to) / denominator, -1F, 1F); + return MathF.Acos(dot); + } + + public static float AngleDegrees(in Vector3F from, in Vector3F to) + { + return AngleRadians(from, to) / MathF.PI * 180f; + } + + public override string ToString() + { + return $"{x}, {y}, {z}"; + } + + public Vector3F Interpolate(Vector3F other, double ratio) => (ratio * this + (1 - ratio) * other).Normalized; + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector3FComparer.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3FComparer.cs new file mode 100644 index 0000000..b0fc5fc --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector3FComparer.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Nanomesh +{ + public class Vector3FComparer : IEqualityComparer + { + private static Vector3FComparer _instance; + public static Vector3FComparer Default => _instance ?? (_instance = new Vector3FComparer(0.001f)); + + private readonly float _tolerance; + + public Vector3FComparer(float tolerance) + { + _tolerance = tolerance; + } + + public bool Equals(Vector3F x, Vector3F y) + { + return (int)(x.x / _tolerance) == (int)(y.x / _tolerance) + && (int)(x.y / _tolerance) == (int)(y.y / _tolerance) + && (int)(x.z / _tolerance) == (int)(y.z / _tolerance); + } + + public int GetHashCode(Vector3F obj) + { + return (int)(obj.x / _tolerance) ^ ((int)(obj.y / _tolerance) << 2) ^ ((int)(obj.z / _tolerance) >> 2); + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector4F.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector4F.cs new file mode 100644 index 0000000..c93b65e --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector4F.cs @@ -0,0 +1,91 @@ +using System; + +namespace Nanomesh +{ + public readonly struct Vector4F : IEquatable, IInterpolable + { + public readonly float x; + public readonly float y; + public readonly float z; + public readonly float w; + + public Vector4F(float x, float y, float z, float w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public float this[int index] + { + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + case 3: return w; + default: + throw new IndexOutOfRangeException("Invalid Vector4F index!"); + } + } + } + + public override int GetHashCode() + { + return Vector4FComparer.Default.GetHashCode(this); + } + + public override bool Equals(object other) + { + if (!(other is Vector4F)) + { + return false; + } + + return Equals((Vector4F)other); + } + + public bool Equals(Vector4F other) + { + return Vector4FComparer.Default.Equals(this, other); + } + + public static Vector4F operator +(in Vector4F a, in Vector4F b) + => new(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + + public static Vector4F operator -(in Vector4F a, in Vector4F b) + => new(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + + public static Vector4F operator *(in Vector4F a, float d) + => new(a.x * d, a.y * d, a.z * d, a.w * d); + + public static Vector4F operator *(float d, in Vector4F a) + => new(a.x * d, a.y * d, a.z * d, a.w * d); + + public static Vector4F operator /(in Vector4F a, float d) + => new(MathUtils.DivideSafe(a.x, d), MathUtils.DivideSafe(a.y, d), MathUtils.DivideSafe(a.z, d), MathUtils.DivideSafe(a.w, d)); + + public static bool operator ==(in Vector4F lhs, in Vector4F rhs) + => Vector4FComparer.Default.Equals(lhs, rhs); + + public static bool operator !=(in Vector4F lhs, in Vector4F rhs) + => !Vector4FComparer.Default.Equals(lhs, rhs); + + public static float Dot(in Vector4F lhs, in Vector4F rhs) + => (lhs.x * rhs.x) + (lhs.y * rhs.y) + (lhs.z * rhs.z) + (lhs.w * rhs.w); + + public Vector4F Interpolate(Vector4F other, double ratio) + { + var t = (float)ratio; + var inv = 1f - t; + return new Vector4F( + (x * inv) + (other.x * t), + (y * inv) + (other.y * t), + (z * inv) + (other.z * t), + (w * inv) + (other.w * t)); + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/Vector4FComparer.cs b/LightlessSync/ThirdParty/Nanomesh/Base/Vector4FComparer.cs new file mode 100644 index 0000000..7bc348d --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/Vector4FComparer.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Nanomesh +{ + public class Vector4FComparer : IEqualityComparer + { + private static Vector4FComparer? _instance; + public static Vector4FComparer Default => _instance ??= new Vector4FComparer(0.0001f); + + private readonly float _tolerance; + + public Vector4FComparer(float tolerance) + { + _tolerance = tolerance; + } + + public bool Equals(Vector4F x, Vector4F y) + { + return (int)(x.x / _tolerance) == (int)(y.x / _tolerance) + && (int)(x.y / _tolerance) == (int)(y.y / _tolerance) + && (int)(x.z / _tolerance) == (int)(y.z / _tolerance) + && (int)(x.w / _tolerance) == (int)(y.w / _tolerance); + } + + public int GetHashCode(Vector4F obj) + { + return (int)(obj.x / _tolerance) + ^ ((int)(obj.y / _tolerance) << 2) + ^ ((int)(obj.z / _tolerance) >> 2) + ^ ((int)(obj.w / _tolerance) << 1); + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Base/VertexData.cs b/LightlessSync/ThirdParty/Nanomesh/Base/VertexData.cs new file mode 100644 index 0000000..9dade3e --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Base/VertexData.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace Nanomesh +{ + public struct VertexData : IEquatable + { + public int position; + public List attributes; // TODO : This is not optimal regarding memory + + public VertexData(int pos) + { + position = pos; + attributes = new List(); + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = hash * 31 + position; + foreach (object attr in attributes) + { + hash = hash * 31 + attr.GetHashCode(); + } + return hash; + } + } + + public bool Equals(VertexData other) + { + if (!position.Equals(other.position)) + return false; + + if (attributes.Count != other.attributes.Count) + return false; + + for (int i = 0; i < attributes.Count; i++) + { + if (!attributes[i].Equals(other.attributes[i])) + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Collections/CollectionUtils.cs b/LightlessSync/ThirdParty/Nanomesh/Collections/CollectionUtils.cs new file mode 100644 index 0000000..ed754bc --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Collections/CollectionUtils.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace Nanomesh +{ + public static class CollectionUtils + { + public static T[] ToArray(this HashSet items, ref T[] array) + { + int i = 0; + foreach (T item in items) + { + array[i++] = item; + } + + return array; + } + + public static bool TryAdd(this Dictionary dictionary, K key, V value) + { + if (dictionary.ContainsKey(key)) + { + return false; + } + + dictionary.Add(key, value); + return true; + } + + public static bool TryAdd(this Dictionary dictionary, K key, Func valueFactory) + { + if (dictionary.ContainsKey(key)) + { + return false; + } + + dictionary.Add(key, valueFactory(key)); + return true; + } + + public static V GetOrAdd(this Dictionary dictionary, K key, V value) + { + if (dictionary.TryGetValue(key, out V existingValue)) + { + return existingValue; + } + + dictionary.Add(key, value); + return value; + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Collections/FastHashSet.cs b/LightlessSync/ThirdParty/Nanomesh/Collections/FastHashSet.cs new file mode 100644 index 0000000..60d4f12 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Collections/FastHashSet.cs @@ -0,0 +1,3872 @@ +//#define Exclude_Check_For_Set_Modifications_In_Enumerator +//#define Exclude_Check_For_Is_Disposed_In_Enumerator +//#define Exclude_No_Hash_Array_Implementation +//#define Exclude_Cache_Optimize_Resize + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Nanomesh +{ + // didn't implement ISerializable and IDeserializationCallback + // these are implemented in the .NET HashSet + // the 7th HashSet constructor has params for serialization -implement that if serialization is implemented + // also add using System.Runtime.Serialization; + + public class FastHashSet : ICollection, IEnumerable, IEnumerable, IReadOnlyCollection, ISet + { + private const int MaxSlotsArraySize = int.MaxValue - 2; + + // this is the size of the non-hash array used to make small counts of items faster + private const int InitialArraySize = 8; + + // this is the # of initial nodes for the slots array after going into hashing after using the noHashArray + // this is 16 + 1; the + 1 is for the first node (node at index 0) which doesn't get used because 0 is the NullIndex + private const int InitialSlotsArraySize = 17; + + // this indicates end of chain if the nextIndex of a node has this value and also indicates no chain if a buckets array element has this value + private const int NullIndex = 0; + + // if a node's nextIndex = this value, then it is a blank node - this isn't a valid nextIndex when unmarked and also when marked (because we don't allow int.MaxValue items) + private const int BlankNextIndexIndicator = int.MaxValue; + + // use this instead of the negate negative logic when getting hashindex - this saves an if (hashindex < 0) which can be the source of bad branch prediction + private const int HighBitNotSet = unchecked(0b0111_1111_1111_1111_1111_1111_1111_1111); + + // The Mark... constants below are for marking, unmarking, and checking if an item is marked. + // This is usefull for some set operations. + + // doing an | (bitwise or) with this and the nextIndex marks the node, setting the bit back will give the original nextIndex value + private const int MarkNextIndexBitMask = unchecked((int)0b1000_0000_0000_0000_0000_0000_0000_0000); + + // doing an & (bitwise and) with this and the nextIndex sets it back to the original value (unmarks it) + private const int MarkNextIndexBitMaskInverted = ~MarkNextIndexBitMask; + + // FastHashSet doesn't allow using an item/node index as high as int.MaxValue. + // There are 2 reasons for this: The first is that int.MaxValue is used as a special indicator + private const int LargestPrimeLessThanMaxInt = 2147483629; + + // these are primes above the .75 loadfactor of the power of 2 except from 30,000 through 80,000, where we conserve space to help with cache space + private static readonly int[] bucketsSizeArray = { 11, 23, 47, 89, 173, 347, 691, 1367, 2741, 5471, 10_937, 19_841/*16_411/*21_851*/, 40_241/*32_771/*43_711*/, 84_463/*65_537/*87_383*/, /*131_101*/174_767, + /*262_147*/349_529, 699_053, 1_398_107, 2_796_221, 5_592_407, 11_184_829, 22_369_661, 44_739_259, 89_478_503, 17_8956_983, 35_7913_951, 715_827_947, 143_1655_777, LargestPrimeLessThanMaxInt}; + + // the buckets array can be pre-allocated to a large size, but it's not good to use that entire size for hashing because of cache locality + // instead do at most 3 size steps (for 3 levels of cache) before using its actual allocated size + + // when an initial capacity is selected in the constructor or later, allocate the required space for the buckets array, but only use a subset of this space until the load factor is met + // limit the # of used elements to optimize for cpu caches + private static readonly int[] bucketsSizeArrayForCacheOptimization = { 3_371, 62_851, 701_819 }; + + private const double LoadFactorConst = .75; + + private int currentIndexIntoBucketsSizeArray; + + private int bucketsModSize; + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + private int incrementForEverySetModification; +#endif + + // resize the buckets array when the count reaches this value + private int resizeBucketsCountThreshold; + + private int count; + + private int nextBlankIndex; + + // this is needed because if items are removed, they get added into the blank list starting at nextBlankIndex, but we may want to TrimExcess capacity, so this is a quick way to see what the ExcessCapacity is + private int firstBlankAtEndIndex; + + private readonly IEqualityComparer comparer; + + // make the buckets size a primary number to make the mod function less predictable + private int[] buckets; + + private TNode[] slots; + +#if !Exclude_No_Hash_Array_Implementation + // used for small sets - when the count of items is small, it is usually faster to just use an array of the items and not do hashing at all (this can also use slightly less memory) + // There may be some cases where the sets can be very small, but there can be very many of these sets. This can be good for these cases. + private T[] noHashArray; +#endif + + internal enum FoundType + { + FoundFirstTime, + FoundNotFirstTime, + NotFound + } + + internal struct TNode + { + // the cached hash code of the item - this is so we don't have to call GetHashCode multiple times, also doubles as a nextIndex for blanks, since blank nodes don't need a hash code + public int hashOrNextIndexForBlanks; + + public int nextIndex; + + public T item; + + public TNode(T elem, int nextIndex, int hash) + { + item = elem; + + this.nextIndex = nextIndex; + + hashOrNextIndexForBlanks = hash; + } + } + + // 1 - same constructor params as HashSet + /// Initializes a new instance of the FastHashSet<>. + /// The element type of the FastHashSet. + public FastHashSet() + { + comparer = EqualityComparer.Default; + SetInitialCapacity(InitialArraySize); + } + + // 2 - same constructor params as HashSet + /// Initializes a new instance of the FastHashSet<>. + /// The element type of the FastHashSet. + /// The collection to initially add to the FastHashSet. + public FastHashSet(IEnumerable collection) + { + comparer = EqualityComparer.Default; + AddInitialEnumerable(collection); + } + + // 3 - same constructor params as HashSet + /// Initializes a new instance of the FastHashSet<>. + /// The element type of the FastHashSet. + /// The IEqualityComparer to use for determining equality of elements in the FastHashSet. + public FastHashSet(IEqualityComparer comparer) + { + this.comparer = comparer ?? EqualityComparer.Default; + SetInitialCapacity(InitialArraySize); + } + + // 4 - same constructor params as HashSet + /// Initializes a new instance of the FastHashSet<>. + /// The element type of the FastHashSet. + /// The initial capacity of the FastHashSet. + public FastHashSet(int capacity) + { + comparer = EqualityComparer.Default; + SetInitialCapacity(capacity); + } + + // 5 - same constructor params as HashSet + /// Initializes a new instance of the FastHashSet<>. + /// The element type of the FastHashSet + /// The collection to initially add to the FastHashSet. + /// The IEqualityComparer to use for determining equality of elements in the FastHashSet. + public FastHashSet(IEnumerable collection, IEqualityComparer comparer) + { + this.comparer = comparer ?? EqualityComparer.Default; + AddInitialEnumerable(collection); + } + + // 6 - same constructor params as HashSet + /// Initializes a new instance of the FastHashSet<>. + /// The element type of the set + /// The initial capacity of the FastHashSet. + /// The IEqualityComparer to use for determining equality of elements in the FastHashSet. + public FastHashSet(int capacity, IEqualityComparer comparer) + { + this.comparer = comparer ?? EqualityComparer.Default; + SetInitialCapacity(capacity); + } + + /// Initializes a new instance of the FastHashSet<>. + /// The element type of the FastHashSet + /// The collection to initially add to the FastHashSet. + /// True if the collection items are all unique. The collection items can be added more quickly if they are known to be unique. + /// The initial capacity of the FastHashSet. + /// The IEqualityComparer to use for determining equality of elements in the FastHashSet. +#if false // removed for now because it's probably not that useful and needs some changes to be correct + public FastHashSet(IEnumerable collection, bool areAllCollectionItemsDefinitelyUnique, int capacity, IEqualityComparer comparer = null) + { + this.comparer = comparer ?? EqualityComparer.Default; + SetInitialCapacity(capacity); + + if (areAllCollectionItemsDefinitelyUnique) + { + // this and the call below must deal correctly with an initial capacity already set + AddInitialUniqueValuesEnumerable(collection); + } + else + { + AddInitialEnumerable(collection); + } + } +#endif + + private void AddInitialUniqueValuesEnumerable(IEnumerable collection) + { + int itemsCount = 0; +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + nextBlankIndex = 1; + foreach (T item in collection) + { + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + int index = buckets[hashIndex]; + buckets[hashIndex] = nextBlankIndex; + + ref TNode t = ref slots[nextBlankIndex]; + + t.hashOrNextIndexForBlanks = hash; + t.nextIndex = index; + t.item = item; + + nextBlankIndex++; + itemsCount++; + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + foreach (T item in collection) + { + noHashArray[itemsCount++] = item; + } + } +#endif + count = itemsCount; + firstBlankAtEndIndex = nextBlankIndex; + } + + private void AddInitialEnumerableWithEnoughCapacity(IEnumerable collection) + { + // this assumes we are hashing + foreach (T item in collection) + { + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + goto Found; // item was found + } + + index = t.nextIndex; + } + + ref TNode tBlank = ref slots[nextBlankIndex]; + + tBlank.hashOrNextIndexForBlanks = hash; + tBlank.nextIndex = buckets[hashIndex]; + tBlank.item = item; + + buckets[hashIndex] = nextBlankIndex; + + nextBlankIndex++; + +#if !Exclude_Cache_Optimize_Resize + count++; + + if (count >= resizeBucketsCountThreshold) + { + ResizeBucketsArrayForward(GetNewBucketsArraySize()); + } +#endif + Found:; + } + firstBlankAtEndIndex = nextBlankIndex; +#if Exclude_Cache_Optimize_Resize + count = nextBlankIndex - 1; +#endif + } + + private void AddInitialEnumerable(IEnumerable collection) + { + FastHashSet fhset = collection as FastHashSet; + if (fhset != null && Equals(fhset.Comparer, Comparer)) + { + // a set with the same item comparer must have all items unique + // so Count will be the exact Count of the items added + // also don't have to check for equals of items + // and a FastHashSet has the additional advantage of not having to call GetHashCode() if it is hashing + // and it has access to the internal slots array so we don't have to use the foreach/enumerator + + int count = fhset.Count; + SetInitialCapacity(count); + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { + if (fhset.IsHashing) + { +#endif + // this FastHashSet is hashing and collection is a FastHashSet (with equal comparer) and it is also hashing + + nextBlankIndex = 1; + int maxNodeIndex = fhset.slots.Length - 1; + if (fhset.firstBlankAtEndIndex <= maxNodeIndex) + { + maxNodeIndex = fhset.firstBlankAtEndIndex - 1; + } + + for (int i = 1; i <= maxNodeIndex; i++) + { + ref TNode t2 = ref fhset.slots[i]; + if (t2.nextIndex != BlankNextIndexIndicator) + { + int hash = t2.hashOrNextIndexForBlanks; + int hashIndex = hash % bucketsModSize; + + ref TNode t = ref slots[nextBlankIndex]; + + t.hashOrNextIndexForBlanks = hash; + t.nextIndex = buckets[hashIndex]; + t.item = t2.item; + + buckets[hashIndex] = nextBlankIndex; + + nextBlankIndex++; + } + } + this.count = count; + firstBlankAtEndIndex = nextBlankIndex; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + // this FastHashSet is hashing and collection is a FastHashSet (with equal comparer) and it is NOT hashing + + nextBlankIndex = 1; + for (int i = 0; i < fhset.count; i++) + { + ref T item = ref noHashArray[i]; + + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + ref TNode t = ref slots[nextBlankIndex]; + + t.hashOrNextIndexForBlanks = hash; + t.nextIndex = buckets[hashIndex]; + t.item = item; + + buckets[hashIndex] = nextBlankIndex; + + nextBlankIndex++; + } + } + } + else + { + // this FastHashSet is not hashing + + AddInitialUniqueValuesEnumerable(collection); + } +#endif + } + else + { + // collection is not a FastHashSet with equal comparer + + HashSet hset = collection as HashSet; + if (hset != null && Equals(hset.Comparer, Comparer)) + { + // a set with the same item comparer must have all items unique + // so Count will be the exact Count of the items added + // also don't have to check for equals of items + + int usedCount = hset.Count; + SetInitialCapacity(usedCount); + + AddInitialUniqueValuesEnumerable(collection); + } + else + { + ICollection coll = collection as ICollection; + if (coll != null) + { + SetInitialCapacity(coll.Count); +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + // call SetInitialCapacity and then set the capacity back to get rid of the excess? + + AddInitialEnumerableWithEnoughCapacity(collection); + + TrimExcess(); +#if !Exclude_No_Hash_Array_Implementation + } + else + { + foreach (T item in collection) + { + Add(item); + } + } +#endif + } + else + { + SetInitialCapacity(InitialArraySize); + + foreach (T item in collection) + { + Add(in item); + } + } + } + } + } + + private void SetInitialCapacity(int capacity) + { +#if !Exclude_No_Hash_Array_Implementation + if (capacity > InitialArraySize) + { +#endif + // skip using the array and go right into hashing + InitHashing(capacity); +#if !Exclude_No_Hash_Array_Implementation + } + else + { + CreateNoHashArray(); // don't set the capacity/size of the noHashArray + } +#endif + } + +#if !Exclude_No_Hash_Array_Implementation + // this function can be called to switch from using the noHashArray and start using the hashing arrays (slots and buckets) + // this function can also be called before noHashArray is even allocated in order to skip using the array and go right into hashing + private void SwitchToHashing(int capacityIncrease = -1) + { + InitHashing(capacityIncrease); + + if (noHashArray != null) + { + // i is the index into noHashArray + for (int i = 0; i < count; i++) + { + ref T item = ref noHashArray[i]; + + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + ref TNode t = ref slots[nextBlankIndex]; + + t.hashOrNextIndexForBlanks = hash; + t.nextIndex = buckets[hashIndex]; + t.item = item; + + buckets[hashIndex] = nextBlankIndex; + + nextBlankIndex++; + } + noHashArray = null; // this array can now be garbage collected because it is no longer referenced + } + + firstBlankAtEndIndex = nextBlankIndex; + } +#endif + + private void InitHashing(int capacity = -1) + { + int newSlotsArraySize; + int newBucketsArraySize; + int newBucketsArrayModSize; + + bool setThresh = false; + if (capacity == -1) + { + newSlotsArraySize = InitialSlotsArraySize; + + newBucketsArraySize = bucketsSizeArray[0]; + if (newBucketsArraySize < newSlotsArraySize) + { + for (currentIndexIntoBucketsSizeArray = 1; currentIndexIntoBucketsSizeArray < bucketsSizeArray.Length; currentIndexIntoBucketsSizeArray++) + { + newBucketsArraySize = bucketsSizeArray[currentIndexIntoBucketsSizeArray]; + if (newBucketsArraySize >= newSlotsArraySize) + { + break; + } + } + } + newBucketsArrayModSize = newBucketsArraySize; + } + else + { + newSlotsArraySize = capacity + 1; // add 1 to accomodate blank first node (node at 0 index) + + newBucketsArraySize = FastHashSetUtil.GetEqualOrClosestHigherPrime((int)(newSlotsArraySize / LoadFactorConst)); + +#if !Exclude_Cache_Optimize_Resize + if (newBucketsArraySize > bucketsSizeArrayForCacheOptimization[0]) + { + newBucketsArrayModSize = bucketsSizeArrayForCacheOptimization[0]; + setThresh = true; + } + else +#endif + { + newBucketsArrayModSize = newBucketsArraySize; + } + } + + if (newSlotsArraySize == 0) + { + // this is an error, the int.MaxValue has been used for capacity and we require more - throw an Exception for this + // could try this with HashSet and see what exception it throws? + throw new InvalidOperationException("Exceeded maximum number of items allowed for this container."); + } + + slots = new TNode[newSlotsArraySize]; // the slots array has an extra item as it's first item (0 index) that is for available items - the memory is wasted, but it simplifies things + buckets = new int[newBucketsArraySize]; // these will be initially set to 0, so make 0 the blank(available) value and reduce all indices by one to get to the actual index into the slots array + bucketsModSize = newBucketsArrayModSize; + + if (setThresh) + { + resizeBucketsCountThreshold = (int)(newBucketsArrayModSize * LoadFactorConst); + } + else + { + CalcUsedItemsLoadFactorThreshold(); + } + + nextBlankIndex = 1; // start at 1 because 0 is the blank item + + firstBlankAtEndIndex = nextBlankIndex; + } + +#if !Exclude_No_Hash_Array_Implementation + private void CreateNoHashArray() + { + noHashArray = new T[InitialArraySize]; + } +#endif + + private void CalcUsedItemsLoadFactorThreshold() + { + if (buckets != null) + { + if (buckets.Length == bucketsModSize) + { + resizeBucketsCountThreshold = slots.Length; // with this value, the buckets array should always resize after the slots array (in the same public function call) + } + else + { + // when buckets.Length > bucketsModSize, this means we want to more slowly increase the bucketsModSize to keep things in the L1-3 caches + resizeBucketsCountThreshold = (int)(bucketsModSize * LoadFactorConst); + } + } + } + + /// True if the FastHashSet if read-only. This is always false. This is only present to implement ICollection, it has no real value otherwise. + bool ICollection.IsReadOnly => false; + + /// Copies all elements of the FastHashSet<> into an array starting at arrayIndex. This implements ICollection.CopyTo(T[], Int32). + /// The destination array. + /// The starting array index to copy elements to. + public void CopyTo(T[] array, int arrayIndex) + { + CopyTo(array, arrayIndex, count); + } + + /// Copies all elements of the FastHashSet<> into an array starting at the first array index. + /// The destination array. + public void CopyTo(T[] array) + { + CopyTo(array, 0, count); + } + + // not really sure how this can be useful because you never know exactly what elements you will get copied (unless you copy them all) + // it could easily vary for different implementations or if items were added in different order or if items were added removed and then added, instead of just added + /// Copies count number of elements of the FastHashSet<> into an array starting at arrayIndex. + /// The destination array. + /// The starting array index to copy elements to. + /// The number of elements to copy. + public void CopyTo(T[] array, int arrayIndex, int count) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array), "Value cannot be null."); + } + + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Non negative number is required."); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Non negative number is required."); + } + + if (arrayIndex + count > array.Length) + { + throw new ArgumentException("Destination array is not long enough to copy all the items in the collection. Check array index and length."); + } + + if (count == 0) + { + return; + } + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int pastNodeIndex = slots.Length; + if (firstBlankAtEndIndex < pastNodeIndex) + { + pastNodeIndex = firstBlankAtEndIndex; + } + + int cnt = 0; + for (int i = 1; i < pastNodeIndex; i++) + { + if (slots[i].nextIndex != BlankNextIndexIndicator) + { + array[arrayIndex++] = slots[i].item; + if (++cnt == count) + { + break; + } + } + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int cnt = this.count; + if (cnt > count) + { + cnt = count; + } + + // for small arrays, I think the for loop below will actually be faster than Array.Copy because of the overhead of that function - could test this + //Array.Copy(noHashArray, 0, array, arrayIndex, cnt); + + for (int i = 0; i < cnt; i++) + { + array[arrayIndex++] = noHashArray[i]; + } + } +#endif + } + + /// + /// Gets the IEqualityComparer used to determine equality for items of this FastHashSet. + /// + public IEqualityComparer Comparer => + // if not set, return the default - this is what HashSet does + // even if it is set to null explicitly, it will still return the default + // this behavior is implmented in the constructor + comparer; + + /// + /// >Gets the number of items in this FastHashSet. + /// + public int Count => count; + + // this is the percent of used items to all items (used + blank/available) + // at which point any additional added items will + // first resize the buckets array to the next prime to avoid too many collisions and chains becoming too large + /// + /// Gets the fraction of 'used items count' divided by 'used items plus available/blank items count'. + /// The buckets array is resized when adding items and this fraction is reached, so this is the minimum LoadFactor for the buckets array. + /// + public double LoadFactor => LoadFactorConst; + + // this is the capacity that can be trimmed with TrimExcessCapacity + // items that were removed from the hash arrays can't be trimmed by calling TrimExcessCapacity, only the blank items at the end + // items that were removed from the noHashArray can be trimmed by calling TrimExcessCapacity because the items after are moved to fill the blank space + /// + /// Gets the capacity that can be trimmed with TrimExcessCapacity. + /// + public int ExcessCapacity + { + get + { + int excessCapacity; +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + excessCapacity = slots.Length - firstBlankAtEndIndex; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + excessCapacity = noHashArray.Length - count; + } +#endif + return excessCapacity; + } + } + + /// + /// Gets the capacity of the FastHashSet, which is the number of elements that can be contained without resizing. + /// + public int Capacity + { + get + { +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + return slots.Length - 1; // subtract 1 for blank node at 0 index +#if !Exclude_No_Hash_Array_Implementation + } + else + { + return noHashArray.Length; + } +#endif + } + } + + /// + /// Gets the size of the next capacity increase of the FastHashSet. + /// + public int NextCapacityIncreaseSize => GetNewSlotsArraySizeIncrease(out int oldSlotsArraySize); + + /// + /// Gets the count of items when the next capacity increase (resize) of the FastHashSet will happen. + /// + public int NextCapacityIncreaseAtCount => resizeBucketsCountThreshold; + + public bool IsHashing => noHashArray == null; + + // the actual capacity at the end of this function may be more than specified + // (in the case when it was more before this function was called - nothing is trimmed by this function, or in the case that slighly more capacity was allocated by this function) + /// + /// Allocate enough space (or make sure existing space is enough) for capacity number of items to be stored in the FastHashSet without any further allocations. + /// + /// The capacity to ensure. + /// The actual capacity at the end of this function. + public int EnsureCapacity(int capacity) + { + // this function is only in .net core for HashSet as of 4/15/2019 +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + + int currentCapacity; + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + currentCapacity = slots.Length - count; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + currentCapacity = noHashArray.Length - count; + } +#endif + + if (currentCapacity < capacity) + { + IncreaseCapacity(capacity - currentCapacity); + } + + // this should be the number where the next lowest number would force a resize of buckets array with the current loadfactor and the entire slots array is full + int calcedNewBucketsArraySize = (int)(slots.Length / LoadFactorConst) + 1; + + if (calcedNewBucketsArraySize < 0 && calcedNewBucketsArraySize > LargestPrimeLessThanMaxInt) + { + calcedNewBucketsArraySize = LargestPrimeLessThanMaxInt; + } + else + { + calcedNewBucketsArraySize = FastHashSetUtil.GetEqualOrClosestHigherPrime(calcedNewBucketsArraySize); + } + + if (buckets.Length < calcedNewBucketsArraySize) + { + // -1 means stop trying to increase the size based on the array of primes + // instead calc based on 2 * existing length and then get the next higher prime + currentIndexIntoBucketsSizeArray = -1; + + ResizeBucketsArrayForward(calcedNewBucketsArraySize); + } + + return slots.Length - count; + } + + // return true if bucketsModSize was set, false otherwise + private bool CheckForModSizeIncrease() + { + if (bucketsModSize < buckets.Length) + { + // instead of array, just have 3 constants + int partLength = (int)(buckets.Length * .75); + + int size0 = bucketsSizeArrayForCacheOptimization[0]; + int size1 = bucketsSizeArrayForCacheOptimization[1]; + if (bucketsModSize == size0) + { + if (size1 <= partLength) + { + bucketsModSize = size1; + return true; + } + else + { + bucketsModSize = buckets.Length; + return true; + } + } + else + { + int size2 = bucketsSizeArrayForCacheOptimization[2]; + if (bucketsModSize == size1) + { + if (size2 <= partLength) + { + bucketsModSize = size2; + return true; + } + else + { + bucketsModSize = buckets.Length; + return true; + } + } + else if (bucketsModSize == size2) + { + bucketsModSize = buckets.Length; + return true; + } + } + } + return false; + } + + private int GetNewSlotsArraySizeIncrease(out int oldArraySize) + { + if (slots != null) + { + oldArraySize = slots.Length; + } + else + { + oldArraySize = InitialSlotsArraySize; // this isn't the old array size, but it is the initial size we should start at + } + + int increaseInSize; + + if (oldArraySize == 1) + { + increaseInSize = InitialSlotsArraySize - 1; + } + else + { + increaseInSize = oldArraySize - 1; + } + + int maxIncreaseInSize = MaxSlotsArraySize - oldArraySize; + + if (increaseInSize > maxIncreaseInSize) + { + increaseInSize = maxIncreaseInSize; + } + return increaseInSize; + } + + // if the value returned gets used and that value is different than the current buckets.Length, then the calling code should increment currentIndexIntoSizeArray because this would now be the current + private int GetNewBucketsArraySize() + { + int newArraySize; + + if (currentIndexIntoBucketsSizeArray >= 0) + { + if (currentIndexIntoBucketsSizeArray + 1 < bucketsSizeArray.Length) + { + newArraySize = bucketsSizeArray[currentIndexIntoBucketsSizeArray + 1]; + } + else + { + newArraySize = buckets.Length; + } + } + else + { + // -1 means stop trying to increase the size based on the array of primes + // instead calc based on 2 * existing length and then get the next higher prime + newArraySize = buckets.Length; + if (newArraySize < int.MaxValue / 2) + { + newArraySize = FastHashSetUtil.GetEqualOrClosestHigherPrime(newArraySize + newArraySize); + } + else + { + newArraySize = LargestPrimeLessThanMaxInt; + } + } + + return newArraySize; + } + + // if hashing, increase the size of the slots array + // if not yet hashing, switch to hashing + private void IncreaseCapacity(int capacityIncrease = -1) + { + // this function might be a fair bit over overhead for resizing at small sizes (like 33 and 65) + // could try to reduce the overhead - there could just be a nextSlotsArraySize (don't need increase?), or nextSlotsArraySizeIncrease? + // then we don't have to call GetNewSlotsArraySizeIncrease at all? + // could test the overhead by just replacing all of the code with +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int newSlotsArraySizeIncrease; + int oldSlotsArraySize; + + if (capacityIncrease == -1) + { + newSlotsArraySizeIncrease = GetNewSlotsArraySizeIncrease(out oldSlotsArraySize); + } + else + { + newSlotsArraySizeIncrease = capacityIncrease; + oldSlotsArraySize = slots.Length; + } + + if (newSlotsArraySizeIncrease <= 0) + { + throw new InvalidOperationException("Exceeded maximum number of items allowed for this container."); + } + + int newSlotsArraySize = oldSlotsArraySize + newSlotsArraySizeIncrease; + + TNode[] newSlotsArray = new TNode[newSlotsArraySize]; + Array.Copy(slots, 0, newSlotsArray, 0, slots.Length); // check the IL, I think Array.Resize and Array.Copy without the start param calls this, so avoid the overhead by calling directly + slots = newSlotsArray; + +#if !Exclude_No_Hash_Array_Implementation + } + else + { + SwitchToHashing(capacityIncrease); + } +#endif + } + + private TNode[] IncreaseCapacityNoCopy(int capacityIncrease = -1) + { +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int newSlotsrraySizeIncrease; + int oldSlotsArraySize; + + if (capacityIncrease == -1) + { + newSlotsrraySizeIncrease = GetNewSlotsArraySizeIncrease(out oldSlotsArraySize); + } + else + { + newSlotsrraySizeIncrease = capacityIncrease; + oldSlotsArraySize = slots.Length; + } + + if (newSlotsrraySizeIncrease <= 0) + { + throw new InvalidOperationException("Exceeded maximum number of items allowed for this container."); + } + + int newSlotsArraySize = oldSlotsArraySize + newSlotsrraySizeIncrease; + + TNode[] newSlotsArray = new TNode[newSlotsArraySize]; + return newSlotsArray; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + SwitchToHashing(capacityIncrease); + return null; + } +#endif + } + + private void ResizeBucketsArrayForward(int newBucketsArraySize) + { + if (newBucketsArraySize == buckets.Length) + { + // this will still work if no increase in size - it just might be slower than if you could increase the buckets array size + } + else + { + if (!CheckForModSizeIncrease()) //??? clean this up, it isn't really good to do it this way - no need to call GetNewBucketsArraySize before calling this function + { + buckets = new int[newBucketsArraySize]; + bucketsModSize = newBucketsArraySize; + + if (currentIndexIntoBucketsSizeArray >= 0) + { + currentIndexIntoBucketsSizeArray++; // when the newBucketsArraySize gets used in the above code, point to the next avaialble size - ??? not sure this is the best place to increment this + } + } + else + { + Array.Clear(buckets, 0, bucketsModSize); + } + + CalcUsedItemsLoadFactorThreshold(); + + int bucketsArrayLength = buckets.Length; + + int pastNodeIndex = slots.Length; + if (firstBlankAtEndIndex < pastNodeIndex) + { + pastNodeIndex = firstBlankAtEndIndex; + } + + //??? for a loop where the end is array.Length, the compiler can skip any array bounds checking - can it do it for this code - it should be able to because pastIndex is no more than buckets.Length + if (firstBlankAtEndIndex == count + 1) + { + // this means there aren't any blank nodes + for (int i = 1; i < pastNodeIndex; i++) + { + ref TNode t = ref slots[i]; + + int hashIndex = t.hashOrNextIndexForBlanks % bucketsArrayLength; + t.nextIndex = buckets[hashIndex]; + + buckets[hashIndex] = i; + } + } + else + { + // this means there are some blank nodes + for (int i = 1; i < pastNodeIndex; i++) + { + ref TNode t = ref slots[i]; + if (t.nextIndex != BlankNextIndexIndicator) // skip blank nodes + { + int hashIndex = t.hashOrNextIndexForBlanks % bucketsArrayLength; + t.nextIndex = buckets[hashIndex]; + + buckets[hashIndex] = i; + } + } + } + } + } + + private void ResizeBucketsArrayForwardKeepMarks(int newBucketsArraySize) + { + if (newBucketsArraySize == buckets.Length) + { + // this will still work if no increase in size - it just might be slower than if you could increase the buckets array size + } + else + { + //??? what if there is a high percent of blank/unused items in the slots array before the firstBlankAtEndIndex (mabye because of lots of removes)? + // It would probably be faster to loop through the buckets array and then do chaining to find the used nodes - one problem with this is that you would have to find blank nodes - but they would be chained + // this probably isn't a very likely scenario + + if (!CheckForModSizeIncrease()) //??? clean this up, it isn't really good to do it this way - no need to call GetNewBucketsArraySize before calling this function + { + buckets = new int[newBucketsArraySize]; + bucketsModSize = newBucketsArraySize; + + if (currentIndexIntoBucketsSizeArray >= 0) + { + currentIndexIntoBucketsSizeArray++; // when the newBucketsArraySize gets used in the above code, point to the next avaialble size - ??? not sure this is the best place to increment this + } + } + + CalcUsedItemsLoadFactorThreshold(); + + int bucketsArrayLength = buckets.Length; + + int pastNodeIndex = slots.Length; + if (firstBlankAtEndIndex < pastNodeIndex) + { + pastNodeIndex = firstBlankAtEndIndex; + } + + //??? for a loop where the end is array.Length, the compiler can skip any array bounds checking - can it do it for this code - it should be able to because pastIndex is no more than buckets.Length + if (firstBlankAtEndIndex == count + 1) + { + // this means there aren't any blank nodes + for (int i = 1; i < pastNodeIndex; i++) + { + ref TNode t = ref slots[i]; + + int hashIndex = t.hashOrNextIndexForBlanks % bucketsArrayLength; + t.nextIndex = buckets[hashIndex] | (t.nextIndex & MarkNextIndexBitMask); + + buckets[hashIndex] = i; + } + } + else + { + // this means there are some blank nodes + for (int i = 1; i < pastNodeIndex; i++) + { + ref TNode t = ref slots[i]; + if (t.nextIndex != BlankNextIndexIndicator) // skip blank nodes + { + int hashIndex = t.hashOrNextIndexForBlanks % bucketsArrayLength; + t.nextIndex = buckets[hashIndex] | (t.nextIndex & MarkNextIndexBitMask); + + buckets[hashIndex] = i; + } + } + } + } + } + + /// + /// Removes all items from the FastHashSet, but does not do any trimming of the resulting unused memory. + /// To trim the unused memory, call TrimExcess. + /// + public void Clear() + { +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) +#endif + { + firstBlankAtEndIndex = 1; + nextBlankIndex = 1; + Array.Clear(buckets, 0, buckets.Length); + } + + count = 0; + } + + // documentation states: + // You can use the TrimExcess method to minimize a HashSet object's memory overhead once it is known that no new elements will be added + // To completely clear a HashSet object and release all memory referenced by it, call this method after calling the Clear method. + /// + /// Trims excess capacity to minimize the FastHashSet's memory overhead. + /// + public void TrimExcess() + { +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + if (slots.Length > firstBlankAtEndIndex && firstBlankAtEndIndex > 0) + { + Array.Resize(ref slots, firstBlankAtEndIndex); + // when firstBlankAtEndIndex == slots.Length, that means there are no blank at end items + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + if (noHashArray != null && noHashArray.Length > count && count > 0) + { + Array.Resize(ref noHashArray, count); + } + } +#endif + } + + // this is only present to implement ICollection - it has no real value otherwise because the Add method with bool return value already does this + /// + /// Implements the ICollection<T> Add method. If possible, use the FastHashSet Add method instead to avoid any slight overhead and return a bool that indicates if the item was added. + /// + /// The item to add. + void ICollection.Add(T item) + { + Add(in item); + } + + // we need 2 versions of Add, one with 'in' and one without 'in' because the one without 'in' is needed to implement the ISet Add method + // always keep the code for these 2 Add methods exactly the same + /// + /// Add an item to the FastHashSet using a read-only reference (in) parameter. Use this version of the Add method when item is a large value type to avoid copying large objects. + /// + /// The item to add. + /// True if the item was added, or false if the FastHashSet already contains the item. + public bool Add(in T item) + { +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + return false; // item was found, so return false to indicate it was not added + } + + index = t.nextIndex; + } + + if (nextBlankIndex >= slots.Length) + { + // there aren't any more blank nodes to add items, so we need to increase capacity + IncreaseCapacity(); + } + + int firstIndex = buckets[hashIndex]; + buckets[hashIndex] = nextBlankIndex; + + ref TNode tBlank = ref slots[nextBlankIndex]; + if (nextBlankIndex >= firstBlankAtEndIndex) + { + // the blank nodes starting at firstBlankAtEndIndex aren't chained + nextBlankIndex = ++firstBlankAtEndIndex; + } + else + { + // the blank nodes before firstBlankAtEndIndex are chained (the hashOrNextIndexForBlanks points to the next blank node) + nextBlankIndex = tBlank.hashOrNextIndexForBlanks; + } + + tBlank.hashOrNextIndexForBlanks = hash; + tBlank.nextIndex = firstIndex; + tBlank.item = item; + + count++; + + if (count >= resizeBucketsCountThreshold) + { + ResizeBucketsArrayForward(GetNewBucketsArraySize()); + } + + return true; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + return false; + } + } + + if (i == noHashArray.Length) + { + SwitchToHashing(); + + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + ref TNode tBlank = ref slots[nextBlankIndex]; + + tBlank.hashOrNextIndexForBlanks = hash; + tBlank.nextIndex = buckets[hashIndex]; + tBlank.item = item; + + buckets[hashIndex] = nextBlankIndex; + + nextBlankIndex = ++firstBlankAtEndIndex; + + count++; + + return true; + } + else + { + // add to noHashArray + noHashArray[i] = item; + count++; + return true; + } + } +#endif + } + + /// + /// Add an item to the FastHashSet. + /// + /// The item to add. + /// True if the item was added, or false if the FastHashSet already contains the item. + public bool Add(T item) + { +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + return false; // item was found, so return false to indicate it was not added + } + + index = t.nextIndex; + } + + if (nextBlankIndex >= slots.Length) + { + // there aren't any more blank nodes to add items, so we need to increase capacity + IncreaseCapacity(); + } + + int firstIndex = buckets[hashIndex]; + buckets[hashIndex] = nextBlankIndex; + + ref TNode tBlank = ref slots[nextBlankIndex]; + if (nextBlankIndex >= firstBlankAtEndIndex) + { + // the blank nodes starting at firstBlankAtEndIndex aren't chained + nextBlankIndex = ++firstBlankAtEndIndex; + } + else + { + // the blank nodes before firstBlankAtEndIndex are chained (the hashOrNextIndexForBlanks points to the next blank node) + nextBlankIndex = tBlank.hashOrNextIndexForBlanks; + } + + tBlank.hashOrNextIndexForBlanks = hash; + tBlank.nextIndex = firstIndex; + tBlank.item = item; + + count++; + + if (count >= resizeBucketsCountThreshold) + { + ResizeBucketsArrayForward(GetNewBucketsArraySize()); + } + + return true; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + return false; + } + } + + if (i == noHashArray.Length) + { + SwitchToHashing(); + + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + ref TNode tBlank = ref slots[nextBlankIndex]; + + tBlank.hashOrNextIndexForBlanks = hash; + tBlank.nextIndex = buckets[hashIndex]; + tBlank.item = item; + + buckets[hashIndex] = nextBlankIndex; + + nextBlankIndex = ++firstBlankAtEndIndex; + + count++; + + return true; + } + else + { + // add to noHashArray + noHashArray[i] = item; + count++; + return true; + } + } +#endif + } + + // return the index in the slots array of the item that was added or found + private int AddToHashSetIfNotFound(in T item, int hash, out bool isFound) + { + // this assmes we are hashing + + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + isFound = true; + return index; // item was found, so return the index of the found item + } + + index = t.nextIndex; + } + + if (nextBlankIndex >= slots.Length) + { + // there aren't any more blank nodes to add items, so we need to increase capacity + IncreaseCapacity(); + ResizeBucketsArrayForward(GetNewBucketsArraySize()); + + // fix things messed up by buckets array resize + hashIndex = hash % bucketsModSize; + } + + int firstIndex = buckets[hashIndex]; + buckets[hashIndex] = nextBlankIndex; + + int addedNodeIndex = nextBlankIndex; + ref TNode tBlank = ref slots[nextBlankIndex]; + if (nextBlankIndex >= firstBlankAtEndIndex) + { + // the blank nodes starting at firstBlankAtEndIndex aren't chained + nextBlankIndex = ++firstBlankAtEndIndex; + } + else + { + // the blank nodes before firstBlankAtEndIndex are chained (the hashOrNextIndexForBlanks points to the next blank node) + nextBlankIndex = tBlank.hashOrNextIndexForBlanks; + } + + tBlank.hashOrNextIndexForBlanks = hash; + tBlank.nextIndex = firstIndex; + tBlank.item = item; + + count++; + + isFound = false; + return addedNodeIndex; // item was not found, so return the index of the added item + } + + // return the node index that was added, or NullIndex if item was found + private int AddToHashSetIfNotFoundAndMark(in T item, int hash) + { + // this assumes we are hashing + + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + return NullIndex; // item was found, so return NullIndex to indicate it was not added + } + + index = t.nextIndex & MarkNextIndexBitMaskInverted; + } + + if (nextBlankIndex >= slots.Length) + { + // there aren't any more blank nodes to add items, so we need to increase capacity + IncreaseCapacity(); + ResizeBucketsArrayForwardKeepMarks(GetNewBucketsArraySize()); + + // fix things messed up by buckets array resize + hashIndex = hash % bucketsModSize; + } + + int firstIndex = buckets[hashIndex]; + buckets[hashIndex] = nextBlankIndex; + + int addedNodeIndex = nextBlankIndex; + ref TNode tBlank = ref slots[nextBlankIndex]; + if (nextBlankIndex >= firstBlankAtEndIndex) + { + // the blank nodes starting at firstBlankAtEndIndex aren't chained + nextBlankIndex = ++firstBlankAtEndIndex; + } + else + { + // the blank nodes before firstBlankAtEndIndex are chained (the hashOrNextIndexForBlanks points to the next blank node) + nextBlankIndex = tBlank.hashOrNextIndexForBlanks; + } + + tBlank.hashOrNextIndexForBlanks = hash; + tBlank.nextIndex = firstIndex | MarkNextIndexBitMask; + tBlank.item = item; + + count++; + + return addedNodeIndex; // item was not found, so return the index of the added item + } + + // we need 2 versions of Contains, one with 'in' and one without 'in' because the one without 'in' is needed to implement the ICollection Contains method + // always keep the code for these 2 Contains methods exactly the same + /// + /// Return true if the item is contained in the FastHashSet, otherwise return false. Use this version of the Contains method when item is a large value type to avoid copying large objects. + /// + /// The item to search for in the FastHashSet. + /// True if found, false if not found. + public bool Contains(in T item) + { +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + return true; // item was found, so return true + } + + index = t.nextIndex; + } + return false; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + return true; // item was found, so return true + } + } + return false; + } +#endif + } + + // this implements Contains for ICollection + /// + /// Return true if the item is contained in the FastHashSet, otherwise return false. + /// + /// The item to search for in the FastHashSet. + /// True if found, false if not found. + public bool Contains(T item) + { +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + return true; // item was found, so return true + } + + index = t.nextIndex; + } + return false; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + return true; // item was found, so return true + } + } + return false; + } +#endif + } + + /// + /// Removes the item from the FastHashSet if found and returns true if the item was found and removed. + /// + /// The item value to remove. + /// True if the item was removed, or false if the item was not contained in the FastHashSet. + public bool Remove(T item) + { +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + int priorIndex = NullIndex; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + // item was found, so remove it + + if (priorIndex == NullIndex) + { + buckets[hashIndex] = t.nextIndex; + } + else + { + slots[priorIndex].nextIndex = t.nextIndex; + } + + // add node to blank chain or to the blanks at the end (if possible) + if (index == firstBlankAtEndIndex - 1) + { + if (nextBlankIndex == firstBlankAtEndIndex) + { + nextBlankIndex--; + } + firstBlankAtEndIndex--; + } + else + { + t.hashOrNextIndexForBlanks = nextBlankIndex; + nextBlankIndex = index; + } + + t.nextIndex = BlankNextIndexIndicator; + + count--; + + return true; + } + + priorIndex = index; + + index = t.nextIndex; + } + return false; // item not found +#if !Exclude_No_Hash_Array_Implementation + } + else + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + // remove the item by moving all remaining items to fill over this one - this is probably faster than Array.CopyTo + for (int j = i + 1; j < count; j++, i++) + { + noHashArray[i] = noHashArray[j]; + } + count--; + return true; + } + } + return false; + } +#endif + } + + // this is a new public method not in HashSet + /// + /// Removes the item from the FastHashSet if found and also if the predicate param evaluates to true on the found item. + /// This is useful if there is something about the found item other than its equality value that can be used to determine if it should be removed. + /// + /// The item value to remove. + /// The predicate to evaluate on the found item. + /// True if the item was removed, or false if the item was not removed. + public bool RemoveIf(in T item, Predicate removeIfPredIsTrue) + { + if (removeIfPredIsTrue == null) + { + throw new ArgumentNullException(nameof(removeIfPredIsTrue), "Value cannot be null."); + } + + // the following code is almost the same as the Remove(item) function except that it additionally invokes the removeIfPredIsTrue param to see if the item should be removed + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + int priorIndex = NullIndex; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + if (removeIfPredIsTrue.Invoke(t.item)) + { + // item was found and predicate was true, so remove it + + if (priorIndex == NullIndex) + { + buckets[hashIndex] = t.nextIndex; + } + else + { + slots[priorIndex].nextIndex = t.nextIndex; + } + + // add node to blank chain or to the blanks at the end (if possible) + if (index == firstBlankAtEndIndex - 1) + { + if (nextBlankIndex == firstBlankAtEndIndex) + { + nextBlankIndex--; + } + firstBlankAtEndIndex--; + } + else + { + t.hashOrNextIndexForBlanks = nextBlankIndex; + nextBlankIndex = index; + } + + t.nextIndex = BlankNextIndexIndicator; + + count--; + + return true; + } + else + { + return false; + } + } + + priorIndex = index; + + index = t.nextIndex; + } + return false; // item not found +#if !Exclude_No_Hash_Array_Implementation + } + else + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + if (removeIfPredIsTrue.Invoke(noHashArray[i])) + { + // remove the item by moving all remaining items to fill over this one - this is probably faster than Array.CopyTo + for (int j = i + 1; j < count; j++, i++) + { + noHashArray[i] = noHashArray[j]; + } + count--; + return true; + } + else + { + return false; + } + } + } + return false; + } +#endif + } + + // this is a new public method not in HashSet + /// + /// Returns a ref to the element in the FastHashSet if found, or adds the item if not present in the FastHashSet and returns a ref to the added element. + /// The returned element reference should only be changed in ways that does not effect its GetHashCode value. + /// The returned element reference should only be used before any modifications to the FastHashSet (like Add or Remove) which may invalidate it. + /// + /// The item to be added or found. + /// Set to true if the item is found, or false if the added was not found and added. + /// Returns a ref to the found item or to the added item. + public ref T FindOrAdd(in T item, out bool isFound) + { +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + + isFound = false; +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int addedOrFoundItemIndex = AddToHashSetIfNotFound(in item, (comparer.GetHashCode(item) & HighBitNotSet), out isFound); + return ref slots[addedOrFoundItemIndex].item; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + isFound = true; + return ref noHashArray[i]; + } + } + + if (i == noHashArray.Length) + { + SwitchToHashing(); + return ref FindOrAdd(in item, out isFound); + } + else + { + // add to noHashArray and keep isAdded true + noHashArray[i] = item; + count++; + return ref noHashArray[i]; + } + } +#endif + } + + // this is a new public method not in HashSet + /// + /// Tries to find the element with the same value as item in the FastHashSet and, if found, it returns a ref to this found element. + /// This is similar to TryGetValue except it returns a ref to the actual element rather than creating copy of the element with an out parameter. + /// This allows the actual element to be changed if it is a mutable value type. + /// The returned element reference should only be changed in ways that does not effect its GetHashCode value. + /// The returned element reference should only be used before any modifications to the FastHashSet (like Add or Remove) which may invalidate it. + /// + /// The item to be found. + /// Set to true if the item is found, or false if not found. + /// Returns a ref to the element if it is found and sets the isFound out parameter to true. If not found, it returns a ref to the first element available and sets the isFound out parameter to false. + public ref T Find(in T item, out bool isFound) + { + isFound = false; +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + FindInSlotsArray(item, out int foundNodeIndex, out int priorNodeIndex, out int bucketsIndex); + if (foundNodeIndex != NullIndex) + { + isFound = true; + } + + return ref slots[foundNodeIndex].item; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + isFound = true; + return ref noHashArray[i]; + } + } + + // if item was not found, still need to return a ref to something, so return a ref to the first item in the array + return ref noHashArray[0]; + } +#endif + } + + // this is a new public method not in HashSet + /// + /// Tries to find the element with the same value as item in the FastHashSet and, if found,it returns a ref to this found element, except if it is also removed (which is determined by the removeIfPredIsTrue parameter). + /// The returned element reference should only be changed in ways that does not effect its GetHashCode value. + /// The returned element reference should only be used before any modifications to the FastHashSet (like Add or Remove) which may invalidate it. + /// + /// + /// The predicate to evaluate on the found item. + /// Set to true if the item is found, or false if not found. + /// Set to true if the item is found and then removed, or false if not removed. + /// Returns a ref to the element if it is found (and not removed) and sets the isFound out parameter to true and the isRemoved out parameter to false. If removed, it returns a reference to the first available element. + public ref T FindAndRemoveIf(in T item, Predicate removeIfPredIsTrue, out bool isFound, out bool isRemoved) + { + if (removeIfPredIsTrue == null) + { + throw new ArgumentNullException(nameof(removeIfPredIsTrue), "Value cannot be null."); + } + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + + isFound = false; + isRemoved = false; + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + FindInSlotsArray(item, out int foundNodeIndex, out int priorNodeIndex, out int bucketsIndex); + if (foundNodeIndex != NullIndex) + { + isFound = true; + ref TNode t = ref slots[foundNodeIndex]; + if (removeIfPredIsTrue.Invoke(t.item)) + { + if (priorNodeIndex == NullIndex) + { + buckets[bucketsIndex] = t.nextIndex; + } + else + { + slots[priorNodeIndex].nextIndex = t.nextIndex; + } + + // add node to blank chain or to the blanks at the end (if possible) + if (foundNodeIndex == firstBlankAtEndIndex - 1) + { + if (nextBlankIndex == firstBlankAtEndIndex) + { + nextBlankIndex--; + } + firstBlankAtEndIndex--; + } + else + { + t.hashOrNextIndexForBlanks = nextBlankIndex; + nextBlankIndex = foundNodeIndex; + } + + t.nextIndex = BlankNextIndexIndicator; + + count--; + + isRemoved = true; + + foundNodeIndex = NullIndex; + } + } + + return ref slots[foundNodeIndex].item; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + isFound = true; + if (removeIfPredIsTrue.Invoke(noHashArray[i])) + { + // remove the item by moving all remaining items to fill over this one - this is probably faster than Array.CopyTo + for (int j = i + 1; j < count; j++, i++) + { + noHashArray[i] = noHashArray[j]; + } + count--; + + isRemoved = true; + return ref noHashArray[0]; + } + else + { + return ref noHashArray[i]; + } + } + } + + // if item was not found, still need to return a ref to something, so return a ref to the first item in the array + return ref noHashArray[0]; + } +#endif + } + + // return index into slots array or 0 if not found + //??? to make things faster, could have a FindInSlotsArray that just returns foundNodeIndex and another version called FindWithPriorInSlotsArray that has the 3 out params + // first test to make sure this works as is + private void FindInSlotsArray(in T item, out int foundNodeIndex, out int priorNodeIndex, out int bucketsIndex) + { + foundNodeIndex = NullIndex; + priorNodeIndex = NullIndex; + + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + bucketsIndex = hashIndex; + + int priorIndex = NullIndex; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + foundNodeIndex = index; + priorNodeIndex = priorIndex; + return; // item was found + } + + priorIndex = index; + + index = t.nextIndex; + } + return; // item not found + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool FindInSlotsArray(in T item, int hash) + { + int hashIndex = hash % bucketsModSize; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + return true; // item was found, so return true + } + + index = t.nextIndex; + } + return false; + } + +#if !Exclude_No_Hash_Array_Implementation + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool FindInNoHashArray(in T item) + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + return true; // item was found, so return true + } + } + return false; + } +#endif + + private void UnmarkAllNextIndexValues(int maxNodeIndex) + { + // must be hashing to be here + for (int i = 1; i <= maxNodeIndex; i++) + { + slots[i].nextIndex &= MarkNextIndexBitMaskInverted; + } + } + + // removeMarked = true, means remove the marked items and keep the unmarked items + // removeMarked = false, means remove the unmarked items and keep the marked items + private void UnmarkAllNextIndexValuesAndRemoveAnyMarkedOrUnmarked(bool removeMarked) + { + // must be hashing to be here + + // must traverse all of the chains instead of just looping through the slots array because going through the chains is the only way to set + // nodes within a chain to blank and still be able to remove the blank node from the chain + + int index; + int nextIndex; + int priorIndex; + int lastNonBlankIndex = firstBlankAtEndIndex - 1; + for (int i = 0; i < buckets.Length; i++) + { + priorIndex = NullIndex; // 0 means use buckets array + index = buckets[i]; + + while (index != NullIndex) + { + ref TNode t = ref slots[index]; + nextIndex = t.nextIndex; + bool isMarked = (nextIndex & MarkNextIndexBitMask) != 0; + if (isMarked) + { + // this node is marked, so unmark it + nextIndex &= MarkNextIndexBitMaskInverted; + t.nextIndex = nextIndex; + } + + if (removeMarked == isMarked) + { + // set this node to blank + + count--; + + // first try to set it to blank by adding it to the blank at end group + if (index == lastNonBlankIndex) + { + //??? does it make sense to attempt this because any already blank items before this will not get added + lastNonBlankIndex--; + if (nextBlankIndex == firstBlankAtEndIndex) + { + nextBlankIndex--; + } + firstBlankAtEndIndex--; + } + else + { + // add to the blank group + + t.nextIndex = BlankNextIndexIndicator; + + t.hashOrNextIndexForBlanks = nextBlankIndex; + nextBlankIndex = index; + } + + if (priorIndex == NullIndex) + { + buckets[i] = nextIndex; + } + else + { + slots[priorIndex].nextIndex = nextIndex; + } + + // keep priorIndex the same because we removed the node in the chain, so the priorIndex is still the same value + } + else + { + priorIndex = index; // node was not removed from the chain, so the priorIndex now points to the node that was not removed + } + + index = nextIndex; + } + } + } + + private FoundType FindInSlotsArrayAndMark(in T item, out int foundNodeIndex) + { + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + int index = buckets[hashIndex]; + + if (index == NullIndex) + { + foundNodeIndex = NullIndex; + return FoundType.NotFound; + } + else + { + // item with same hashIndex already exists, so need to look in the chained list for an equal item (using Equals) + + int nextIndex; + while (true) + { + ref TNode t = ref slots[index]; + nextIndex = t.nextIndex; + + // check if hash codes are equal before calling Equals (which may take longer) items that are Equals must have the same hash code + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + foundNodeIndex = index; + if ((nextIndex & MarkNextIndexBitMask) == 0) + { + // not marked, so mark it + t.nextIndex |= MarkNextIndexBitMask; + + return FoundType.FoundFirstTime; + } + return FoundType.FoundNotFirstTime; + } + + nextIndex &= MarkNextIndexBitMaskInverted; + if (nextIndex == NullIndex) + { + foundNodeIndex = NullIndex; + return FoundType.NotFound; // not found + } + else + { + index = nextIndex; + } + } + } + } + + // this is a new public method not in HashSet + /// + /// Get the information about the size of chains in the FastHashSet. + /// The size of chains should be small to reduce traversing and comparing items. + /// This can indicate the effectiveness of the hash code creation method. + /// + /// Outputs the average node visits per chain. This is a single number that summarizes the average length of chains in terms of the average number of compares until an equal value is found (when the item is present). + /// A List of LevelAndCount items that gives the length of each chain in the FastHashSet. + public List GetChainLevelsCounts(out double avgNodeVisitsPerChain) + { + Dictionary itemsInChainToCountDict = new Dictionary(); + + // this function only makes sense when hashing + int chainCount = 0; + if (buckets != null) + { + for (int i = 0; i < buckets.Length; i++) + { + int index = buckets[i]; + if (index != NullIndex) + { + chainCount++; + int itemsInChain = 1; + + while (slots[index].nextIndex != NullIndex) + { + index = slots[index].nextIndex; + itemsInChain++; + } + + itemsInChainToCountDict.TryGetValue(itemsInChain, out int cnt); + cnt++; + itemsInChainToCountDict[itemsInChain] = cnt; + } + } + } + + double totalAvgNodeVisitsIfVisitingAllChains = 0; + List lst = new List(itemsInChainToCountDict.Count); + foreach (KeyValuePair keyVal in itemsInChainToCountDict) + { + lst.Add(new ChainLevelAndCount(keyVal.Key, keyVal.Value)); + if (keyVal.Key == 1) + { + totalAvgNodeVisitsIfVisitingAllChains += keyVal.Value; + } + else + { + totalAvgNodeVisitsIfVisitingAllChains += keyVal.Value * (keyVal.Key + 1.0) / 2.0; + } + } + + if (chainCount == 0) + { + avgNodeVisitsPerChain = 0; + } + else + { + avgNodeVisitsPerChain = totalAvgNodeVisitsIfVisitingAllChains / chainCount; + } + + lst.Sort(); + + return lst; + } + + // this is a new public method not in HashSet + /// + /// Reorders items in the same hash chain (items that have the same hash code or mod to the same index), so that they are adjacent in memory. + /// This gives better locality of reference for larger count of items, which can result in fewer cache misses. + /// + public void ReorderChainedNodesToBeAdjacent() + { + if (slots != null) + { + TNode[] newSlotsArray = new TNode[slots.Length]; + + // copy elements using the buckets array chains so there is better locality in the chains + int index; + int newIndex = 1; + for (int i = 0; i < buckets.Length; i++) + { + index = buckets[i]; + if (index != NullIndex) + { + buckets[i] = newIndex; + while (true) + { + ref TNode t = ref slots[index]; + ref TNode tNew = ref newSlotsArray[newIndex]; + index = t.nextIndex; + newIndex++; + + // copy + tNew.hashOrNextIndexForBlanks = t.hashOrNextIndexForBlanks; + tNew.item = t.item; + if (index == NullIndex) + { + tNew.nextIndex = NullIndex; + break; + } + tNew.nextIndex = newIndex; + } + } + } + + newIndex++; + nextBlankIndex = newIndex; + firstBlankAtEndIndex = newIndex; + slots = newSlotsArray; + } + } + + /// + /// Looks for equalValue and if found, returns a copy of the found value in actualValue and returns true. + /// + /// The item to look for. + /// The copy of the found value, if found, or the default value of the same type if not found. + /// True if equalValue is found, or false if not found. + public bool TryGetValue(T equalValue, out T actualValue) + { +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + FindInSlotsArray(equalValue, out int foundNodeIndex, out int priorNodeIndex, out int bucketsIndex); + if (foundNodeIndex > 0) + { + actualValue = slots[foundNodeIndex].item; + return true; + } + + actualValue = default; + return false; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + for (i = 0; i < count; i++) + { + if (comparer.Equals(equalValue, noHashArray[i])) + { + actualValue = noHashArray[i]; + return true; + } + } + + actualValue = default; + return false; + } +#endif + } + + /// + /// Adds all items in into this FastHashSet. This is similar to AddRange for other types of collections, but it is called UnionWith for ISets. + /// + /// The enumerable items to add (cannot be null). + public void UnionWith(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + // Note: HashSet doesn't seem to increment this unless it really changes something - like doing an Add(3) when 3 is already in the hashset doesn't increment, same as doing a UnionWith with an empty set as the param. +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + + if (other == this) + { + return; + } + + //??? maybe there is a faster way to add a bunch at one time - I copied the Add code below to make this faster + //foreach (T item in range) + //{ + // Add(item); + //} + + // do this with more code because it might get called in some high performance situations + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + foreach (T item in other) + { + AddToHashSetIfNotFound(in item, (comparer.GetHashCode(item) & HighBitNotSet), out bool isFound); + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + + foreach (T item in other) + { + //??? if it's easier for the jit compiler or il compiler to remove the array bounds checking then + // have i < noHashArray.Length and do the check for count within the loop with a break statement + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + goto found; // break out of inner for loop + } + } + + // if here then item was not found + if (i == noHashArray.Length) + { + SwitchToHashing(); + AddToHashSetIfNotFound(in item, (comparer.GetHashCode(item) & HighBitNotSet), out bool isFound); + } + else + { + // add to noHashArray + noHashArray[i] = item; + count++; + } + + found:; + } + } +#endif + } + + /// + /// Removes all items in from the FastHashSet. + /// + /// The enumerable items (cannot be null). + public void ExceptWith(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + if (other == this) + { + Clear(); + } + else + { + foreach (T item in other) + { + Remove(item); + } + } + } + + /// + /// Removes items from the FastHashSet so that the only remaining items are those contained in that also match an item in the FastHashSet. + /// + /// The enumerable items (cannot be null). + public void IntersectWith(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + return; + } + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + + // if hashing, find each item in the slots array and mark anything found, but remove from being found again + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int foundItemCount = 0; // the count of found items in the hash - without double counting + foreach (T item in other) + { + FoundType foundType = FindInSlotsArrayAndMark(in item, out int foundIndex); + if (foundType == FoundType.FoundFirstTime) + { + foundItemCount++; + + if (foundItemCount == count) + { + break; + } + } + } + + if (foundItemCount == 0) + { + Clear(); + } + else + { + UnmarkAllNextIndexValuesAndRemoveAnyMarkedOrUnmarked(false); + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + // Note: we could actually do this faster by moving any found items to the front and keeping track of the found items + // with a single int index + // the problem with this method is it reorders items and even though that shouldn't matter in a set + // it might cause issues with code that incorrectly assumes order stays the same for operations like this + + // possibly a faster implementation would be to use the method above, but keep track of original order with an int array of the size of count (ex. item at 0 was originally 5, and also item at 5 was originally 0) + + // set the corresponding bit in this int if an item was found + // using a uint means the no hashing array cannot be more than 32 items + uint foundItemBits = 0; + + int i; + + int foundItemCount = 0; // the count of found items in the hash - without double counting + foreach (T item in other) + { + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + uint mask = (1u << i); + if ((foundItemBits & mask) == 0) + { + foundItemBits |= mask; + foundItemCount++; + } + goto found; // break out of inner for loop + } + } + + found: + if (foundItemCount == count) + { + // all items in the set were found, so there is nothing to remove - the set isn't changed + return; + } + } + + if (foundItemCount == 0) + { + count = 0; // this is the equivalent of calling Clear + } + else + { + // remove any items that are unmarked (unfound) + // go backwards because this can be faster + for (i = count - 1; i >= 0; i--) + { + uint mask = (1u << i); + if ((foundItemBits & mask) == 0) + { + if (i < count - 1) + { + // a faster method if there are multiple unfound items in a row is to find the first used item (make i go backwards until the item is used and then increment i by 1) + // if there aren't multiple unused in a row, then this is a bit of a waste + + int j = i + 1; // j now points to the next item after the unfound one that we want to keep + + i--; + while (i >= 0) + { + uint mask2 = (1u << i); + if ((foundItemBits & mask2) != 0) + { + break; + } + i--; + } + i++; + + int k = i; + for (; j < count; j++, k++) + { + noHashArray[k] = noHashArray[j]; + } + } + + count--; + } + } + } + } +#endif + } + + // An empty set is a proper subset of any other collection. Therefore, this method returns true if the collection represented by the current HashSet object + // is empty unless the other parameter is also an empty set. + // This method always returns false if Count is greater than or equal to the number of elements in other. + // If the collection represented by other is a HashSet collection with the same equality comparer as the current HashSet object, + // then this method is an O(n) operation. Otherwise, this method is an O(n + m) operation, where n is Count and m is the number of elements in other. + + /// + /// Returns true if this FastHashSet is a proper subset of . + /// + /// The enumerable items (cannot be null). + /// True if a proper subset of . + public bool IsProperSubsetOf(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + return false; + } + + ICollection collection = other as ICollection; + if (collection != null) + { + if (count == 0 && collection.Count > 0) + { + return true; // by definition, an empty set is a proper subset of any non-empty collection + } + + if (count >= collection.Count) + { + return false; + } + } + else + { + if (count == 0) + { + foreach (T item in other) + { + return true; + } + return false; + } + } + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int foundItemCount = 0; // the count of found items in the hash - without double counting + int maxFoundIndex = 0; + bool notFoundAtLeastOne = false; + foreach (T item in other) + { + FoundType foundType = FindInSlotsArrayAndMark(in item, out int foundIndex); + if (foundType == FoundType.FoundFirstTime) + { + foundItemCount++; + if (maxFoundIndex < foundIndex) + { + maxFoundIndex = foundIndex; + } + } + else if (foundType == FoundType.NotFound) + { + notFoundAtLeastOne = true; + } + + if (notFoundAtLeastOne && foundItemCount == count) + { + // true means all of the items in the set were found in other and at least one item in other was not found in the set + break; // will return true below after unmarking + } + } + + UnmarkAllNextIndexValues(maxFoundIndex); + + return notFoundAtLeastOne && foundItemCount == count; // true if all of the items in the set were found in other and at least one item in other was not found in the set +#if !Exclude_No_Hash_Array_Implementation + } + else + { + uint foundItemBits = 0; + + int foundItemCount = 0; // the count of found items in the hash - without double counting + bool notFoundAtLeastOne = false; + foreach (T item in other) + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + uint mask = (1u << i); + if ((foundItemBits & mask) == 0) + { + foundItemBits |= mask; + foundItemCount++; + } + goto found; // break out of inner for loop + } + } + + // if here then item was not found + notFoundAtLeastOne = true; + + found: + if (notFoundAtLeastOne && foundItemCount == count) + { + // true means all of the items in the set were found in other and at least one item in other was not found in the set + return true; + } + } + + return false; + } +#endif + } + + /// + /// Returns true if this FastHashSet is a subset of . + /// + /// The enumerable items (cannot be null). + /// True if a subset of . + public bool IsSubsetOf(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + return true; + } + + if (count == 0) + { + return true; // by definition, an empty set is a subset of any collection + } + + ICollection collection = other as ICollection; + if (collection != null) + { + if (count > collection.Count) + { + return false; + } + } + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int foundItemCount = 0; // the count of found items in the hash - without double counting + int maxFoundIndex = 0; + foreach (T item in other) + { + FoundType foundType = FindInSlotsArrayAndMark(in item, out int foundIndex); + if (foundType == FoundType.FoundFirstTime) + { + foundItemCount++; + if (maxFoundIndex < foundIndex) + { + maxFoundIndex = foundIndex; + } + + if (foundItemCount == count) + { + break; + } + } + } + + UnmarkAllNextIndexValues(maxFoundIndex); + + return foundItemCount == count; // true if all of the items in the set were found in other +#if !Exclude_No_Hash_Array_Implementation + } + else + { + uint foundItemBits = 0; + + int foundItemCount = 0; // the count of found items in the hash - without double counting + foreach (T item in other) + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + uint mask = (1u << i); + if ((foundItemBits & mask) == 0) + { + foundItemBits |= mask; + foundItemCount++; + } + goto found; // break out of inner for loop + } + } + + found: + if (foundItemCount == count) + { + break; + } + } + + return foundItemCount == count; // true if all of the items in the set were found in other + } +#endif + } + + /// + /// Returns true if this FastHashSet is a proper superset of . + /// + /// The enumerable items (cannot be null). + /// True if a proper superset of . + public bool IsProperSupersetOf(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + return false; + } + + if (count == 0) + { + return false; // an empty set can never be a proper superset of anything (not even an empty collection) + } + + ICollection collection = other as ICollection; + if (collection != null) + { + if (collection.Count == 0) + { + return true; // by definition, an empty other means the set is a proper superset of it if the set has at least one value + } + } + else + { + foreach (T item in other) + { + goto someItemsInOther; + } + return true; + } + + someItemsInOther: + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int foundItemCount = 0; // the count of found items in the hash - without double counting + int maxFoundIndex = 0; + foreach (T item in other) + { + FoundType foundType = FindInSlotsArrayAndMark(in item, out int foundIndex); + if (foundType == FoundType.FoundFirstTime) + { + foundItemCount++; + if (maxFoundIndex < foundIndex) + { + maxFoundIndex = foundIndex; + } + + if (foundItemCount == count) + { + break; + } + } + else if (foundType == FoundType.NotFound) + { + // any unfound item means this can't be a proper superset of + UnmarkAllNextIndexValues(maxFoundIndex); + return false; + } + } + + UnmarkAllNextIndexValues(maxFoundIndex); + + return foundItemCount < count; // true if all of the items in other were found in set and at least one item in set was not found in other +#if !Exclude_No_Hash_Array_Implementation + } + else + { + uint foundItemBits = 0; + + int foundItemCount = 0; // the count of found items in the hash - without double counting + foreach (T item in other) + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + uint mask = (1u << i); + if ((foundItemBits & mask) == 0) + { + foundItemBits |= mask; + foundItemCount++; + } + goto found; // break out of inner for loop + } + } + + // if here then item was not found + return false; + + found: + if (foundItemCount == count) + { + break; + } + } + + return foundItemCount < count; // true if all of the items in other were found in set and at least one item in set was not found in other + } +#endif + } + + /// + /// Returns true if this FastHashSet is a superset of . + /// + /// The enumerable items (cannot be null). + /// True if a superset of . + public bool IsSupersetOf(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + return true; + } + + ICollection collection = other as ICollection; + if (collection != null) + { + if (collection.Count == 0) + { + return true; // by definition, an empty other means the set is a superset of it + } + } + else + { + foreach (T item in other) + { + goto someItemsInOther; + } + return true; + } + + someItemsInOther: + + if (count == 0) + { + return false; // an empty set can never be a proper superset of anything (except an empty collection - but an empty collection returns true above) + } + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + foreach (T item in other) + { + if (!FindInSlotsArray(in item, (comparer.GetHashCode(item) & HighBitNotSet))) + { + return false; + } + } + + return true; // true if all of the items in other were found in the set, false if at least one item in other was not found in the set +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + + foreach (T item in other) + { + for (i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + goto found; // break out of inner for loop + } + } + + // if here then item was not found + return false; + + found:; + + } + + return true; // true if all of the items in other were found in the set, false if at least one item in other was not found in the set + } +#endif + } + + /// + /// Returns true if this FastHashSet contains any items in . + /// + /// The enumerable items (cannot be null). + /// True if contains any items in . + public bool Overlaps(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + return count > 0; // return false if there are no items when both sets are the same, otherwise return true when both sets are the same + } + + foreach (T item in other) + { + if (Contains(in item)) + { + return true; + } + } + return false; + } + + /// + /// Returns true if this FastHashSet contains exactly the same elements as . + /// + /// The enumerable items (cannot be null). + /// True if contains the same elements as . + public bool SetEquals(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + return true; + } + + // if other is ICollection, then it has count + + ICollection c = other as ICollection; + + if (c != null) + { + if (c.Count < count) + { + return false; + } + + HashSet hset = other as HashSet; + if (hset != null && Equals(hset.Comparer, Comparer)) + { + if (hset.Count != count) + { + return false; + } + + foreach (T item in other) + { + if (!Contains(in item)) + { + return false; + } + } + return true; + } + + FastHashSet fhset = other as FastHashSet; + if (fhset != null && Equals(fhset.Comparer, Comparer)) + { + if (fhset.Count != count) + { + return false; + } + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int pastNodeIndex = slots.Length; + if (firstBlankAtEndIndex < pastNodeIndex) + { + pastNodeIndex = firstBlankAtEndIndex; + } + +#if !Exclude_No_Hash_Array_Implementation + if (fhset.IsHashing) + { +#endif + for (int i = 1; i < pastNodeIndex; i++) + { + // could not do the blank check if we know there aren't any blanks - below code and in the loop in the else + // could do the check to see if there are any blanks first and then have 2 versions of this code, one with the check for blank and the other without it + if (slots[i].nextIndex != BlankNextIndexIndicator) // skip any blank nodes + { + if (!fhset.FindInSlotsArray(in slots[i].item, slots[i].hashOrNextIndexForBlanks)) + { + return false; + } + } + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + for (int i = 1; i < pastNodeIndex; i++) + { + if (slots[i].nextIndex != BlankNextIndexIndicator) // skip any blank nodes + { + if (!fhset.FindInNoHashArray(in slots[i].item)) + { + return false; + } + } + } + } + } + else + { + foreach (T item in other) + { + if (!FindInNoHashArray(in item)) + { + return false; + } + } + } + return true; +#endif + } + + } + + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + int foundItemCount = 0; // the count of found items in the hash - without double counting + int maxFoundIndex = 0; + foreach (T item in other) + { + FoundType foundType = FindInSlotsArrayAndMark(in item, out int foundIndex); + if (foundType == FoundType.FoundFirstTime) + { + foundItemCount++; + if (maxFoundIndex < foundIndex) + { + maxFoundIndex = foundIndex; + } + } + else if (foundType == FoundType.NotFound) + { + UnmarkAllNextIndexValues(maxFoundIndex); + return false; + } + } + + UnmarkAllNextIndexValues(maxFoundIndex); + + return foundItemCount == count; +#if !Exclude_No_Hash_Array_Implementation + } + else + { + uint foundItemBits = 0; + + int foundItemCount = 0; // the count of found items in the hash - without double counting + foreach (T item in other) + { + for (int i = 0; i < count; i++) + { + if (comparer.Equals(item, noHashArray[i])) + { + uint mask = (1u << i); + if ((foundItemBits & mask) == 0) + { + foundItemBits |= mask; + foundItemCount++; + } + goto found; // break out of inner for loop + } + } + // if here then item was not found + return false; + found:; + } + + return foundItemCount == count; + } +#endif + } + + // From the online document: Modifies the current HashSet object to contain only elements that are present either in that object or in the specified collection, but not both. + /// + /// Modifies the FastHashSet so that it contains only items in the FashHashSet or , but not both. + /// So items in that are also in the FastHashSet are removed, and items in that are not in the FastHashSet are added. + /// + /// The enumerable items (cannot be null). + public void SymmetricExceptWith(IEnumerable other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other), "Value cannot be null."); + } + + if (other == this) + { + Clear(); + } + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (!IsHashing) + { + // to make things easier for now, just switch to hashing if calling this function and deal with only one set of code + SwitchToHashing(); + } +#endif + + // for the first loop through other, add any unfound items and mark + int addedNodeIndex; + int maxAddedNodeIndex = NullIndex; + foreach (T item in other) + { + addedNodeIndex = AddToHashSetIfNotFoundAndMark(in item, (comparer.GetHashCode(item) & HighBitNotSet)); + if (addedNodeIndex > maxAddedNodeIndex) + { + maxAddedNodeIndex = addedNodeIndex; + } + } + + foreach (T item in other) + { + RemoveIfNotMarked(in item); + } + + UnmarkAllNextIndexValues(maxAddedNodeIndex); + } + + private void RemoveIfNotMarked(in T item) + { + // calling this function assumes we are hashing + int hash = (comparer.GetHashCode(item) & HighBitNotSet); + int hashIndex = hash % bucketsModSize; + + int priorIndex = NullIndex; + + for (int index = buckets[hashIndex]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + if (t.hashOrNextIndexForBlanks == hash && comparer.Equals(t.item, item)) + { + // item was found, so remove it if not marked + if ((t.nextIndex & MarkNextIndexBitMask) == 0) + { + if (priorIndex == NullIndex) + { + buckets[hashIndex] = t.nextIndex; + } + else + { + // if slots[priorIndex].nextIndex was marked, then keep it marked + // already know that t.nextIndex is not marked + slots[priorIndex].nextIndex = t.nextIndex | (slots[priorIndex].nextIndex & MarkNextIndexBitMask); + } + + // add node to blank chain or to the blanks at the end (if possible) + if (index == firstBlankAtEndIndex - 1) + { + if (nextBlankIndex == firstBlankAtEndIndex) + { + nextBlankIndex--; + } + firstBlankAtEndIndex--; + } + else + { + t.hashOrNextIndexForBlanks = nextBlankIndex; + nextBlankIndex = index; + } + + t.nextIndex = BlankNextIndexIndicator; + + count--; + + return; + } + } + + priorIndex = index; + + index = t.nextIndex & MarkNextIndexBitMaskInverted; + } + return; // item not found + } + + /// + /// Removes any items in the FastHashSet where the predicate is true for that item. + /// + /// The match predicate (cannot be null). + /// The number of items removed. + public int RemoveWhere(Predicate match) + { + if (match == null) + { + throw new ArgumentNullException(nameof(match), "Value cannot be null."); + } + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification++; +#endif + + int removeCount = 0; + +#if !Exclude_No_Hash_Array_Implementation + if (IsHashing) + { +#endif + // must traverse all of the chains instead of just looping through the slots array because going through the chains is the only way to set + // nodes within a chain to blank and still be able to remove the blank node from the chain + + int priorIndex; + int nextIndex; + for (int i = 0; i < buckets.Length; i++) + { + priorIndex = NullIndex; // 0 means use buckets array + + for (int index = buckets[i]; index != NullIndex;) + { + ref TNode t = ref slots[index]; + + nextIndex = t.nextIndex; + if (match.Invoke(t.item)) + { + // item was matched, so remove it + + if (priorIndex == NullIndex) + { + buckets[i] = nextIndex; + } + else + { + slots[priorIndex].nextIndex = nextIndex; + } + + // add node to blank chain or to the blanks at the end (if possible) + if (index == firstBlankAtEndIndex - 1) + { + if (nextBlankIndex == firstBlankAtEndIndex) + { + nextBlankIndex--; + } + firstBlankAtEndIndex--; + } + else + { + t.hashOrNextIndexForBlanks = nextBlankIndex; + nextBlankIndex = index; + } + + t.nextIndex = BlankNextIndexIndicator; + + count--; + removeCount++; + } + + priorIndex = index; + + index = nextIndex; + } + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + int i; + for (i = count - 1; i >= 0; i--) + { + if (match.Invoke(noHashArray[i])) + { + removeCount++; + + if (i < count - 1) + { + int j = i + 1; + int k = i; + for (; j < count; j++, k++) + { + noHashArray[k] = noHashArray[j]; + } + } + + count--; + } + } + } +#endif + + return removeCount; + } + + private class FastHashSetEqualityComparer : IEqualityComparer> + { + public bool Equals(FastHashSet x, FastHashSet y) + { + if (x == null && y == null) + { + return true; + } + + if (y == null) + { + return false; + } + + if (x != null) + { + return x.SetEquals(y); + } + else + { + return false; + } + } + + public int GetHashCode(FastHashSet set) + { + if (set == null) + { + // oddly the documentation for the IEqualityComparer.GetHashCode function says it will throw an ArgumentNullException if the param is null + return 0; // 0 seems to be what .NET framework uses when passing in null, so return the same thing to be consistent + } + else + { + unchecked + { + int hashCode = 0; +#if !Exclude_No_Hash_Array_Implementation + if (set.IsHashing) + { +#endif + int pastNodeIndex = set.slots.Length; + if (set.firstBlankAtEndIndex < pastNodeIndex) + { + pastNodeIndex = set.firstBlankAtEndIndex; + } + + for (int i = 1; i < pastNodeIndex; i++) + { + if (set.slots[i].nextIndex != 0) // nextIndex == 0 indicates a blank/available node + { + // maybe do ^= instead of add? - will this produce the same thing regardless of order? - if ^= maybe we don't need unchecked + // sum up the individual item hash codes - this way it won't matter what order the items are in, the same resulting hash code will be produced + hashCode += set.slots[i].hashOrNextIndexForBlanks; + } + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + for (int i = 0; i < set.count; i++) + { + // sum up the individual item hash codes - this way it won't matter what order the items are in, the same resulting hash code will be produced + hashCode += set.noHashArray[i].GetHashCode(); + } + } +#endif + return hashCode; + } + } + } + } + + /// + /// Creates and returns the IEqualityComparer for a FastHashSet which can be used to compare two FastHashSets based on their items being equal. + /// + /// An IEqualityComparer for a FastHashSet. + public static IEqualityComparer> CreateSetComparer() + { + return new FastHashSetEqualityComparer(); + } + + /// + /// Allows enumerating through items in the FastHashSet. Order is not guaranteed. + /// + /// The IEnumerator for the FastHashSet. + public IEnumerator GetEnumerator() + { + return new FastHashSetEnumerator(this); + } + + /// + /// Allows enumerating through items in the FastHashSet. Order is not guaranteed. + /// + /// The IEnumerator for the FastHashSet. + IEnumerator IEnumerable.GetEnumerator() + { + return new FastHashSetEnumerator(this); + } + + private class FastHashSetEnumerator : IEnumerator + { + private readonly FastHashSet set; + private int currentIndex = -1; + +#if !Exclude_Check_For_Is_Disposed_In_Enumerator + private bool isDisposed; +#endif + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + private readonly int incrementForEverySetModification; +#endif + + /// + /// Constructor for the FastHashSetEnumerator that takes a FastHashSet as a parameter. + /// + /// The FastHashSet to enumerate through. + public FastHashSetEnumerator(FastHashSet set) + { + this.set = set; +#if !Exclude_No_Hash_Array_Implementation + if (set.IsHashing) + { +#endif + currentIndex = NullIndex; // 0 is the index before the first possible node (0 is the blank node) +#if !Exclude_No_Hash_Array_Implementation + } + else + { + currentIndex = -1; + } +#endif + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + incrementForEverySetModification = set.incrementForEverySetModification; +#endif + } + + /// + /// Moves to the next item for the FastHashSet enumerator. + /// + /// True if there was a next item, otherwise false. + public bool MoveNext() + { +#if !Exclude_Check_For_Is_Disposed_In_Enumerator + if (isDisposed) + { + // the only reason this code returns false when Disposed is called is to be compatable with HashSet + // if this level of compatibility isn't needed, then #define Exclude_Check_For_Is_Disposed_In_Enumerator to remove this check and makes the code slightly faster + return false; + } +#endif + +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + if (incrementForEverySetModification != set.incrementForEverySetModification) + { + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + } +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (set.IsHashing) + { +#endif + // it's easiest to just loop through the node array and skip any nodes that are blank + // rather than looping through the buckets array and following the nextIndex to the end of each bucket + + while (true) + { + currentIndex++; + if (currentIndex < set.firstBlankAtEndIndex) + { + if (set.slots[currentIndex].nextIndex != BlankNextIndexIndicator) + { + return true; + } + } + else + { + currentIndex = set.firstBlankAtEndIndex; + return false; + } + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + currentIndex++; + if (currentIndex < set.count) + { + return true; + } + else + { + currentIndex--; + return false; + } + } +#endif + } + + /// + /// Resets the FastHashSet enumerator. + /// + public void Reset() + { +#if !Exclude_Check_For_Set_Modifications_In_Enumerator + if (incrementForEverySetModification != set.incrementForEverySetModification) + { + throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); + } +#endif + +#if !Exclude_No_Hash_Array_Implementation + if (set.IsHashing) + { +#endif + currentIndex = NullIndex; // 0 is the index before the first possible node (0 is the blank node) +#if !Exclude_No_Hash_Array_Implementation + } + else + { + currentIndex = -1; + } +#endif + } + + /// + /// Implements the IDisposable.Dispose method for the FastHashSet enumerator. + /// + void IDisposable.Dispose() + { +#if !Exclude_Check_For_Is_Disposed_In_Enumerator + isDisposed = true; +#endif + } + + /// + /// Gets the current item for the FastHashSet enumerator. + /// + public T2 Current + { + get + { +#if !Exclude_No_Hash_Array_Implementation + if (set.IsHashing) + { +#endif + // it's easiest to just loop through the node array and skip any nodes with nextIndex = 0 + // rather than looping through the buckets array and following the nextIndex to the end of each bucket + + if (currentIndex > NullIndex && currentIndex < set.firstBlankAtEndIndex) + { + return set.slots[currentIndex].item; + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + if (currentIndex >= 0 && currentIndex < set.count) + { + return set.noHashArray[currentIndex]; + } + } +#endif + return default; + } + } + + /// + /// Gets a reference to the current item for the FastHashSet enumerator. + /// + public ref T2 CurrentRef + { + get + { +#if !Exclude_No_Hash_Array_Implementation + if (set.IsHashing) + { +#endif + // it's easiest to just loop through the node array and skip any nodes with nextIndex = 0 + // rather than looping through the buckets array and following the nextIndex to the end of each bucket + + if (currentIndex > NullIndex && currentIndex < set.firstBlankAtEndIndex) + { + return ref set.slots[currentIndex].item; + } + else + { + // we can just return a ref to the 0 node's item instead of throwing an exception? - this should have a default item value + return ref set.slots[0].item; + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + if (currentIndex >= 0 && currentIndex < set.count) + { + return ref set.noHashArray[currentIndex]; + } + else + { + // we can just return a ref to the 0 node's item instead of throwing an exception? + return ref set.noHashArray[0]; + } + } +#endif + } + } + + /// + /// True if the current item is valid for the FastHashSet enumerator, otherwise false. + /// + public bool IsCurrentValid + { + get + { +#if !Exclude_No_Hash_Array_Implementation + if (set.IsHashing) + { +#endif + // it's easiest to just loop through the node array and skip any nodes with nextIndex = 0 + // rather than looping through the buckets array and following the nextIndex to the end of each bucket + + if (currentIndex > NullIndex && currentIndex < set.firstBlankAtEndIndex) + { + return true; + } +#if !Exclude_No_Hash_Array_Implementation + } + else + { + if (currentIndex >= 0 && currentIndex < set.count) + { + return true; + } + } +#endif + return false; + } + } + + /// + /// Gets the Current item for the FastHashSet enumerator. + /// + object IEnumerator.Current => Current; + } + + public static class FastHashSetUtil + { + /// + /// Return the prime number that is equal to n (if n is a prime number) or the closest prime number greather than n. + /// + /// The lowest number to start looking for a prime. + /// The passed in n parameter value (if it is prime), or the next highest prime greater than n. + public static int GetEqualOrClosestHigherPrime(int n) + { + if (n >= LargestPrimeLessThanMaxInt) + { + // the next prime above this number is int.MaxValue, which we don't want to return that value because some indices increment one or two ints past this number and we don't want them to overflow + return LargestPrimeLessThanMaxInt; + } + + if ((n & 1) == 0) + { + n++; // make n odd + } + + bool found; + + do + { + found = true; + + int sqrt = (int)Math.Sqrt(n); + for (int i = 3; i <= sqrt; i += 2) + { + int div = n / i; + if (div * i == n) // dividing and multiplying might be faster than a single % (n % i) == 0 + { + found = false; + n += 2; + break; + } + } + } while (!found); + + return n; + } + } + } + + public struct ChainLevelAndCount : IComparable + { + public ChainLevelAndCount(int level, int count) + { + Level = level; + Count = count; + } + + public int Level; + public int Count; + + public int CompareTo(ChainLevelAndCount other) + { + return Level.CompareTo(other.Level); + } + } + +#if DEBUG + public static class DebugOutput + { + public static void OutputEnumerableItems(IEnumerable e, string enumerableName) + { + System.Diagnostics.Debug.WriteLine("---start items: " + enumerableName + "---"); + int count = 0; + foreach (T2 item in e) + { + System.Diagnostics.Debug.WriteLine(item.ToString()); + count++; + } + System.Diagnostics.Debug.WriteLine("---end items: " + enumerableName + "; count = " + count.ToString("N0") + "---"); + } + + public static void OutputSortedEnumerableItems(IEnumerable e, string enumerableName) + { + List lst = new List(e); + lst.Sort(); + System.Diagnostics.Debug.WriteLine("---start items (sorted): " + enumerableName + "---"); + int count = 0; + foreach (T2 item in lst) + { + System.Diagnostics.Debug.WriteLine(item.ToString()); + count++; + } + System.Diagnostics.Debug.WriteLine("---end items: " + enumerableName + "; count = " + count.ToString("N0") + "---"); + } + } +#endif +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Collections/LinkedHashSet.cs b/LightlessSync/ThirdParty/Nanomesh/Collections/LinkedHashSet.cs new file mode 100644 index 0000000..a72f4d3 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Collections/LinkedHashSet.cs @@ -0,0 +1,565 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nanomesh +{ + public class LinkedHashSet : IReadOnlyCollection where T : IComparable + { + private readonly Dictionary> elements; + private LinkedHashNode first, last; + + /// + /// Initializes a new instance of the class. + /// + public LinkedHashSet() + { + elements = new Dictionary>(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + public LinkedHashSet(IEnumerable initialValues) : this() + { + UnionWith(initialValues); + } + + public LinkedHashNode First => first; + + public LinkedHashNode Last => last; + + #region Implementation of IEnumerable + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + /// 2 + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region Implementation of ICollection + + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + public int Count => elements.Count; + + /// + /// Removes all items from the . + /// + /// The is read-only. + public void Clear() + { + elements.Clear(); + first = null; + last = null; + } + + /// + /// Determines whether the contains a specific value. + /// + /// + /// true if is found in the ; otherwise, false. + /// + /// The object to locate in the . + public bool Contains(T item) + { + return elements.ContainsKey(item); + } + + /// + /// Copies the elements of the to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing.The zero-based index in at which copying begins. is null. is less than 0. is multidimensional.-or-The number of elements in the source is greater than the available space from to the end of the destination .-or-Type cannot be cast automatically to the type of the destination . + public void CopyTo(T[] array, int arrayIndex) + { + int index = arrayIndex; + + foreach (T item in this) + { + array[index++] = item; + } + } + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// + /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . + /// + /// The object to remove from the .The is read-only. + public bool Remove(T item) + { + if (elements.TryGetValue(item, out LinkedHashNode node)) + { + elements.Remove(item); + Unlink(node); + return true; + } + + return false; + } + + #endregion + + + #region Implementation of ISet + + /// + /// Modifies the current set so that it contains all elements that are present in either the current set or the specified collection. + /// + /// The collection to compare to the current set. is null. + public void UnionWith(IEnumerable other) + { + foreach (T item in other) + { + Add(item); + } + } + + /// + /// Modifies the current set so that it contains only elements that are also in a specified collection. + /// + /// The collection to compare to the current set. is null. + public void IntersectWith(IEnumerable other) + { + ISet otherSet = AsSet(other); + + LinkedHashNode current = first; + while (current != null) + { + if (!otherSet.Contains(current.Value)) + { + elements.Remove(current.Value); + Unlink(current); + } + current = current.Next; + } + } + + /// + /// Removes all elements in the specified collection from the current set. + /// + /// The collection of items to remove from the set. is null. + public void ExceptWith(IEnumerable other) + { + foreach (T item in other) + { + Remove(item); + } + } + + /// + /// Modifies the current set so that it contains only elements that are present either in the current set or in the specified collection, but not both. + /// + /// The collection to compare to the current set. is null. + public void SymmetricExceptWith(IEnumerable other) + { + foreach (T item in other) + { + if (elements.TryGetValue(item, out LinkedHashNode node)) + { + elements.Remove(item); + Unlink(node); + } + else + { + Add(item); + } + } + } + + /// + /// Determines whether the current set is a superset of a specified collection. + /// + /// + /// true if the current set is a superset of ; otherwise, false. + /// + /// The collection to compare to the current set. is null. + public bool IsSupersetOf(IEnumerable other) + { + int numberOfOthers = CountOthers(other, out int numberOfOthersPresent); + + // All others must be present. + return numberOfOthersPresent == numberOfOthers; + } + + /// + /// Determines whether the current set is a correct superset of a specified collection. + /// + /// + /// true if the object is a correct superset of ; otherwise, false. + /// + /// The collection to compare to the current set. is null. + public bool IsProperSupersetOf(IEnumerable other) + { + int numberOfOthers = CountOthers(other, out int numberOfOthersPresent); + + // All others must be present, plus we need to have at least one additional item. + return numberOfOthersPresent == numberOfOthers && numberOfOthers < Count; + } + + /// + /// Determines whether the current set and the specified collection contain the same elements. + /// + /// + /// true if the current set is equal to ; otherwise, false. + /// + /// The collection to compare to the current set. is null. + public bool SetEquals(IEnumerable other) + { + int numberOfOthers = CountOthers(other, out int numberOfOthersPresent); + + return numberOfOthers == Count && numberOfOthersPresent == Count; + } + + /// + /// Adds an element to the current set and returns a value to indicate if the element was successfully added. + /// + /// + /// true if the element is added to the set; false if the element is already in the set. + /// + /// The element to add to the set. + public bool Add(T item) + { + if (elements.ContainsKey(item)) + { + return false; + } + + LinkedHashNode node = new LinkedHashNode(item) { Previous = last }; + + if (first == null) + { + first = node; + } + + if (last != null) + { + last.Next = node; + } + + last = node; + + elements.Add(item, node); + + return true; + } + + public bool AddAfter(T item, LinkedHashNode itemInPlace) + { + if (elements.ContainsKey(item)) + { + return false; + } + + LinkedHashNode node = new LinkedHashNode(item) { Previous = itemInPlace }; + + if (itemInPlace.Next != null) + { + node.Next = itemInPlace.Next; + itemInPlace.Next.Previous = node; + } + else + { + last = node; + } + + itemInPlace.Next = node; + + elements.Add(item, node); + + return true; + } + + public bool PushAfter(T item, LinkedHashNode itemInPlace) + { + if (elements.ContainsKey(item)) + { + return false; + } + + LinkedHashNode node = Last; + Unlink(node); + elements.Remove(node.Value); + node.Value = item; + node.Next = null; + node.Previous = itemInPlace; + + if (itemInPlace.Next != null) + { + node.Next = itemInPlace.Next; + itemInPlace.Next.Previous = node; + } + else + { + last = node; + } + + itemInPlace.Next = node; + + elements.Add(item, node); + + return true; + } + + public bool AddBefore(T item, LinkedHashNode itemInPlace) + { + if (elements.ContainsKey(item)) + { + return false; + } + + LinkedHashNode node = new LinkedHashNode(item) { Next = itemInPlace }; + + if (itemInPlace.Previous != null) + { + node.Previous = itemInPlace.Previous; + itemInPlace.Previous.Next = node; + } + else + { + first = node; + } + + itemInPlace.Previous = node; + + elements.Add(item, node); + + return true; + } + + public bool PushBefore(T item, LinkedHashNode itemInPlace) + { + if (elements.ContainsKey(item)) + { + return false; + } + + LinkedHashNode node = Last; + Unlink(node); + elements.Remove(node.Value); + node.Value = item; + node.Previous = null; + node.Next = itemInPlace; + + if (itemInPlace.Previous != null) + { + node.Previous = itemInPlace.Previous; + itemInPlace.Previous.Next = node; + } + else + { + first = node; + } + + itemInPlace.Previous = node; + + elements.Add(item, node); + + return true; + } + + #endregion + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An struct that can be used to iterate through the collection. + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + + /// + /// Count the elements in the given collection and determine both the total + /// count and how many of the elements that are present in the current set. + /// + private int CountOthers(IEnumerable items, out int numberOfOthersPresent) + { + numberOfOthersPresent = 0; + int numberOfOthers = 0; + + foreach (T item in items) + { + numberOfOthers++; + if (Contains(item)) + { + numberOfOthersPresent++; + } + } + return numberOfOthers; + } + + + /// + /// Cast the given collection to an ISet<T> if possible. If not, + /// return a new set containing the items. + /// + private static ISet AsSet(IEnumerable items) + { + return items as ISet ?? new HashSet(items); + } + + + /// + /// Unlink a node from the linked list by updating the node pointers in + /// its preceeding and subsequent node. Also update the _first and _last + /// pointers if necessary. + /// + private void Unlink(LinkedHashNode node) + { + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + + if (ReferenceEquals(node, first)) + { + first = node.Next; + } + + if (ReferenceEquals(node, last)) + { + last = node.Previous; + } + } + + public class LinkedHashNode + { + public TElement Value; + public LinkedHashNode Next; + public LinkedHashNode Previous; + + public LinkedHashNode(TElement value) + { + Value = value; + } + + public override string ToString() + { + return Value.ToString(); + } + } + + public struct Enumerator : IEnumerator + { + private LinkedHashNode _node; + private T _current; + + internal Enumerator(LinkedHashSet set) + { + _current = default(T); + _node = set.first; + } + + /// + public bool MoveNext() + { + if (_node == null) + { + return false; + } + + _current = _node.Value; + _node = _node.Next; + return true; + } + + /// + public T Current => _current; + + /// + object IEnumerator.Current => Current; + + /// + void IEnumerator.Reset() + { + throw new NotSupportedException(); + } + + /// + public void Dispose() + { + } + } + + public void AddMin(T item) + { + LinkedHashNode current = Last; + while (current != null && item.CompareTo(current.Value) < 0) + { + current = current.Previous; + } + + if (current == Last) + { + return; + } + + if (current == null) + { + AddBefore(item, First); + } + else + { + AddAfter(item, current); + } + } + + public void PushMin(T item) + { + LinkedHashNode current = Last; + while (current != null && item.CompareTo(current.Value) < 0) + { + current = current.Previous; + } + + if (current == Last) + { + return; + } + + if (current == null) + { + PushBefore(item, First); + } + else + { + PushAfter(item, current); + } + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Collections/MaxHeap.cs b/LightlessSync/ThirdParty/Nanomesh/Collections/MaxHeap.cs new file mode 100644 index 0000000..9164ffa --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Collections/MaxHeap.cs @@ -0,0 +1,86 @@ +using System; + +namespace Nanomesh +{ + public static class MaxHeap + { + public static T FindKthLargest(T[] nums, int k) where T : IComparable + { + Heap heap = new Heap(); + heap.Heapify(nums, nums.Length); + T data = default(T); + for (int i = 0; i < k; i++) + { + data = heap.RemoveMax(); + } + return data; + } + } + + public class Heap where T : IComparable + { + private T[] arr; + private int count; + private int size; + + public int GetLeftChild(int pos) + { + int l = 2 * pos + 1; + return l >= count ? -1 : l; + } + + public int GetRightChild(int pos) + { + int r = 2 * pos + 2; + return r >= count ? -1 : r; + } + + public void Heapify(T[] num, int n) + { + arr = new T[n]; + size = n; + for (int i = 0; i < n; i++) + { + arr[i] = num[i]; + } + + count = n; + + for (int i = (count - 1) / 2; i >= 0; i--) + { + PercolateDown(i); + } + } + public void PercolateDown(int pos) + { + int l = GetLeftChild(pos); + int r = GetRightChild(pos); + int max = pos; + if (l != -1 && arr[max].CompareTo(arr[l]) < 0) + { + max = l; + } + + if (r != -1 && arr[max].CompareTo(arr[r]) < 0) + { + max = r; + } + + if (max != pos) + { + T temp = arr[pos]; + arr[pos] = arr[max]; + arr[max] = temp; + PercolateDown(max); + } + } + public T RemoveMax() + { + T data = arr[0]; + arr[0] = arr[count - 1]; + count--; + PercolateDown(0); + return data; + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Collections/MinHeap.cs b/LightlessSync/ThirdParty/Nanomesh/Collections/MinHeap.cs new file mode 100644 index 0000000..8b318c5 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Collections/MinHeap.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Nanomesh.Collections +{ + public class MinHeap : IEnumerable + { + private readonly List values; + private readonly IComparer comparer; + + public MinHeap(IEnumerable items, IComparer comparer) + { + values = new List(); + this.comparer = comparer; + values.Add(default(T)); + values.AddRange(items); + + for (int i = values.Count / 2; i >= 1; i--) + { + BubbleDown(i); + } + } + + public MinHeap(IEnumerable items) : this(items, Comparer.Default) { } + + public MinHeap(IComparer comparer) : this(new T[0], comparer) { } + + public MinHeap() : this(Comparer.Default) { } + + public int Count => values.Count - 1; + + public T Min => values[1]; + + /// + /// Extract the smallest element. + /// + /// + public T ExtractMin() + { + int count = Count; + + if (count == 0) + { + throw new InvalidOperationException("Heap is empty."); + } + + T min = Min; + values[1] = values[count]; + values.RemoveAt(count); + + if (values.Count > 1) + { + BubbleDown(1); + } + + return min; + } + + /// + /// Insert the value. + /// + /// + /// + public void Add(T item) + { + values.Add(item); + BubbleUp(Count); + } + + private void BubbleUp(int index) + { + int parent = index / 2; + + while (index > 1 && CompareResult(parent, index) > 0) + { + Exchange(index, parent); + index = parent; + parent /= 2; + } + } + + private void BubbleDown(int index) + { + int min; + + while (true) + { + int left = index * 2; + int right = index * 2 + 1; + + if (left < values.Count && + CompareResult(left, index) < 0) + { + min = left; + } + else + { + min = index; + } + + if (right < values.Count && + CompareResult(right, min) < 0) + { + min = right; + } + + if (min != index) + { + Exchange(index, min); + index = min; + } + else + { + return; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int CompareResult(int index1, int index2) + { + return comparer.Compare(values[index1], values[index2]); + } + + private void Exchange(int index, int max) + { + T tmp = values[index]; + values[index] = values[max]; + values[max] = tmp; + } + + public IEnumerator GetEnumerator() + { + return values.Skip(1).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Collections/OrderStatistics.cs b/LightlessSync/ThirdParty/Nanomesh/Collections/OrderStatistics.cs new file mode 100644 index 0000000..dbc3726 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Collections/OrderStatistics.cs @@ -0,0 +1,118 @@ +using System; + +namespace Nanomesh +{ + public static class OrderStatistics + { + private static T FindMedian(T[] arr, int i, int n) + { + if (i <= n) + { + Array.Sort(arr, i, n); // Sort the array + } + else + { + Array.Sort(arr, n, i); + } + + return arr[n / 2]; // Return middle element + } + + // Returns k'th smallest element + // in arr[l..r] in worst case + // linear time. ASSUMPTION: ALL + // ELEMENTS IN ARR[] ARE DISTINCT + public static T FindKthSmallest(T[] arr, int l, int r, int k) where T : IComparable + { + // If k is smaller than + // number of elements in array + if (k > 0 && k <= r - l + 1) + { + int n = r - l + 1; // Number of elements in arr[l..r] + + // Divide arr[] in groups of size 5, + // calculate median of every group + // and store it in median[] array. + int i; + + // There will be floor((n+4)/5) groups; + T[] median = new T[(n + 4) / 5]; + for (i = 0; i < n / 5; i++) + { + median[i] = FindMedian(arr, l + i * 5, 5); + } + + // For last group with less than 5 elements + if (i * 5 < n) + { + median[i] = FindMedian(arr, l + i * 5, n % 5); + i++; + } + + // Find median of all medians using recursive call. + // If median[] has only one element, then no need + // of recursive call + T medOfMed = (i == 1) ? median[i - 1] : FindKthSmallest(median, 0, i - 1, i / 2); + + // Partition the array around a random element and + // get position of pivot element in sorted array + int pos = Partition(arr, l, r, medOfMed); + + // If position is same as k + if (pos - l == k - 1) + { + return arr[pos]; + } + + if (pos - l > k - 1) // If position is more, recur for left + { + return FindKthSmallest(arr, l, pos - 1, k); + } + + // Else recur for right subarray + return FindKthSmallest(arr, pos + 1, r, k - pos + l - 1); + } + + // If k is more than number of elements in array + return default(T); + } + + private static void Swap(ref T[] arr, int i, int j) + { + T temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + + // It searches for x in arr[l..r], and + // partitions the array around x. + private static int Partition(T[] arr, int l, int r, T x) where T : IComparable + { + // Search for x in arr[l..r] and move it to end + int i; + for (i = l; i < r; i++) + { + if (arr[i].CompareTo(x) == 0) + { + break; + } + } + + Swap(ref arr, i, r); + + // Standard partition algorithm + i = l; + for (int j = l; j <= r - 1; j++) + { + if (arr[j].CompareTo(x) <= 0) + { + Swap(ref arr, i, j); + i++; + } + } + Swap(ref arr, i, r); + return i; + } + + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/AttributeDefinition.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/AttributeDefinition.cs new file mode 100644 index 0000000..3a60ca8 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/AttributeDefinition.cs @@ -0,0 +1,30 @@ +namespace Nanomesh +{ + public struct AttributeDefinition + { + public double weight; + public AttributeType type; + public int id; + + public AttributeDefinition(AttributeType type) + { + this.weight = 1; + this.type = type; + this.id = 0; + } + + public AttributeDefinition(AttributeType type, double weight) + { + this.weight = weight; + this.type = type; + this.id = 0; + } + + public AttributeDefinition(AttributeType type, double weight, int id) + { + this.weight = weight; + this.type = type; + this.id = id; + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/AttributeType.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/AttributeType.cs new file mode 100644 index 0000000..24d596a --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/AttributeType.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Nanomesh +{ + public enum AttributeType + { + Normals, + UVs, + BoneWeights, + Colors, + } + + public static class AttributeUtils + { + public static MetaAttributeList CreateAttributesFromDefinitions(IList attributeDefinitions) + { + MetaAttributeList attributeList = new EmptyMetaAttributeList(0); + for (int i = 0; i < attributeDefinitions.Count; i++) + { + switch (attributeDefinitions[i].type) + { + case AttributeType.Normals: + attributeList = attributeList.AddAttributeType(); + break; + case AttributeType.UVs: + attributeList = attributeList.AddAttributeType(); + break; + case AttributeType.BoneWeights: + attributeList = attributeList.AddAttributeType(); + break; + default: + throw new NotImplementedException(); + } + } + return attributeList; + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/MetaAttribute.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/MetaAttribute.cs new file mode 100644 index 0000000..a08c16a --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/MetaAttribute.cs @@ -0,0 +1,406 @@ +using System; + +namespace Nanomesh +{ + public unsafe interface IMetaAttribute + { + IMetaAttribute Set(int index, K value) where K : unmanaged; + K Get(int index) where K : unmanaged; + } + + public unsafe struct MetaAttribute : IMetaAttribute + where T0 : unmanaged + { + public T0 attr0; + + public MetaAttribute(T0 attr0) + { + this.attr0 = attr0; + } + + public unsafe K Get(int index) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + return kk[0]; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + public IMetaAttribute Set(int index, K value) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override int GetHashCode() + { + return attr0.GetHashCode(); + } + + public override bool Equals(object obj) + { + return ((MetaAttribute)obj).attr0.Equals(attr0); + } + } + + public unsafe struct MetaAttribute : IMetaAttribute + where T0 : unmanaged + where T1 : unmanaged + { + public T0 attr0; + public T1 attr1; + + public MetaAttribute(T0 attr0, T1 attr1) + { + this.attr0 = attr0; + this.attr1 = attr1; + } + + public unsafe K Get(int index) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + return kk[0]; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + return kk[0]; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + public IMetaAttribute Set(int index, K value) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public unsafe struct MetaAttribute : IMetaAttribute + where T0 : unmanaged + where T1 : unmanaged + where T2 : unmanaged + { + public T0 attr0; + public T1 attr1; + public T2 attr2; + + public MetaAttribute(T0 attr0, T1 attr1, T2 attr2) + { + this.attr0 = attr0; + this.attr1 = attr1; + this.attr2 = attr2; + } + + public unsafe K Get(int index) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + return kk[0]; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + return kk[0]; + } + case 2: + fixed (T2* k = &attr2) + { + K* kk = (K*)k; + return kk[0]; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + public IMetaAttribute Set(int index, K value) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 2: + fixed (T2* k = &attr2) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public unsafe struct MetaAttribute : IMetaAttribute + where T0 : unmanaged + where T1 : unmanaged + where T2 : unmanaged + where T3 : unmanaged + { + public T0 attr0; + public T1 attr1; + public T2 attr2; + public T3 attr3; + + public MetaAttribute(T0 attr0, T1 attr1, T2 attr2, T3 attr3) + { + this.attr0 = attr0; + this.attr1 = attr1; + this.attr2 = attr2; + this.attr3 = attr3; + } + + public unsafe K Get(int index) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + return kk[0]; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + return kk[0]; + } + case 2: + fixed (T2* k = &attr2) + { + K* kk = (K*)k; + return kk[0]; + } + case 3: + fixed (T3* k = &attr3) + { + K* kk = (K*)k; + return kk[0]; + } + default: + throw new ArgumentOutOfRangeException(); + } + + // Shorter idea but only C# 8.0: + //fixed (void* v = &this) + //{ + // byte* b = (byte*)v; + // b += Positions[index]; + // return ((K*)b)[0]; + //}; + } + + public IMetaAttribute Set(int index, K value) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 2: + fixed (T2* k = &attr2) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 3: + fixed (T3* k = &attr3) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public unsafe struct MetaAttribute : IMetaAttribute + where T0 : unmanaged + where T1 : unmanaged + where T2 : unmanaged + where T3 : unmanaged + where T4 : unmanaged + { + public T0 attr0; + public T1 attr1; + public T2 attr2; + public T3 attr3; + public T4 attr4; + + public MetaAttribute(T0 attr0, T1 attr1, T2 attr2, T3 attr3, T4 attr4) + { + this.attr0 = attr0; + this.attr1 = attr1; + this.attr2 = attr2; + this.attr3 = attr3; + this.attr4 = attr4; + } + + public unsafe K Get(int index) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + return kk[0]; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + return kk[0]; + } + case 2: + fixed (T2* k = &attr2) + { + K* kk = (K*)k; + return kk[0]; + } + case 3: + fixed (T3* k = &attr3) + { + K* kk = (K*)k; + return kk[0]; + } + case 4: + fixed (T4* k = &attr4) + { + K* kk = (K*)k; + return kk[0]; + } + default: + throw new ArgumentOutOfRangeException(); + } + + // Shorter idea but only C# 8.0: + //fixed (void* v = &this) + //{ + // byte* b = (byte*)v; + // b += Positions[index]; + // return ((K*)b)[0]; + //}; + } + + public IMetaAttribute Set(int index, K value) where K : unmanaged + { + switch (index) + { + case 0: + fixed (T0* k = &attr0) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 1: + fixed (T1* k = &attr1) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 2: + fixed (T2* k = &attr2) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 3: + fixed (T3* k = &attr3) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + case 4: + fixed (T4* k = &attr4) + { + K* kk = (K*)k; + kk[0] = value; + return this; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/MetaAttributeList.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/MetaAttributeList.cs new file mode 100644 index 0000000..048969c --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/Attributes/MetaAttributeList.cs @@ -0,0 +1,448 @@ +using System; + +namespace Nanomesh +{ + public abstract class MetaAttributeList + { + public abstract IMetaAttribute this[int index] + { + get; + set; + } + + public abstract int Count { get; } + + public abstract int CountPerAttribute { get; } + + public abstract MetaAttributeList CreateNew(int length); + + public abstract MetaAttributeList AddAttributeType() + where T : unmanaged, IInterpolable; + + public abstract bool Equals(int indexA, int indexB, int attribute); + + public abstract void Interpolate(int attribute, int indexA, int indexB, double ratio); + } + + public class EmptyMetaAttributeList : MetaAttributeList + { + private readonly int _length; + + public EmptyMetaAttributeList(int length) + { + _length = length; + } + + public override IMetaAttribute this[int index] + { + get => throw new System.Exception(); + set => throw new System.Exception(); + } + + public override MetaAttributeList CreateNew(int length) => new EmptyMetaAttributeList(length); + + public override unsafe bool Equals(int indexA, int indexB, int attribute) + { + return false; + } + + public override void Interpolate(int attribute, int indexA, int indexB, double ratio) + { + throw new System.Exception(); + } + + public override MetaAttributeList AddAttributeType() + { + return new MetaAttributeList(_length); + } + + public override int Count => 0; + + public override int CountPerAttribute => 0; + } + + public class MetaAttributeList : MetaAttributeList + where T0 : unmanaged, IInterpolable + { + private readonly MetaAttribute[] _attributes; + + public MetaAttributeList(int length) + { + _attributes = new MetaAttribute[length]; + } + + public override IMetaAttribute this[int index] + { + get => _attributes[index]; + set => _attributes[index] = (MetaAttribute)value; + } + + public void Set(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + private void Get(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + public override MetaAttributeList CreateNew(int length) => new MetaAttributeList(length); + + public override unsafe bool Equals(int indexA, int indexB, int attribute) + { + switch (attribute) + { + case 0: + return _attributes[indexA].Get(0).Equals(_attributes[indexB].Get(0)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override void Interpolate(int attribute, int indexA, int indexB, double ratio) + { + _attributes[indexA].attr0 = _attributes[indexA].Get(0).Interpolate(_attributes[indexB].Get(0), ratio); + _attributes[indexB].attr0 = _attributes[indexA].attr0; + } + + public override MetaAttributeList AddAttributeType() + { + MetaAttributeList newAttributes = new MetaAttributeList(_attributes.Length); + for (int i = 0; i < Count; i++) + newAttributes.Set(new MetaAttribute(_attributes[i].attr0, default(T)), i); + return newAttributes; + } + + public override int Count => _attributes.Length; + + public override int CountPerAttribute => 1; + } + + public class MetaAttributeList : MetaAttributeList + where T0 : unmanaged, IInterpolable + where T1 : unmanaged, IInterpolable + { + private readonly MetaAttribute[] _attributes; + + public MetaAttributeList(int length) + { + _attributes = new MetaAttribute[length]; + } + + public override IMetaAttribute this[int index] + { + get => _attributes[index]; + set => _attributes[index] = (MetaAttribute)value; + } + + public void Set(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + private void Get(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + public override MetaAttributeList CreateNew(int length) => new MetaAttributeList(length); + + public override unsafe bool Equals(int indexA, int indexB, int attribute) + { + switch (attribute) + { + case 0: + return _attributes[indexA].Get(0).Equals(_attributes[indexB].Get(0)); + case 1: + return _attributes[indexA].Get(1).Equals(_attributes[indexB].Get(1)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override void Interpolate(int attribute, int indexA, int indexB, double ratio) + { + switch (attribute) + { + case 0: + _attributes[indexA].attr0 = _attributes[indexA].Get(0).Interpolate(_attributes[indexB].Get(0), ratio); + _attributes[indexB].attr0 = _attributes[indexA].attr0; + break; + case 1: + _attributes[indexA].attr1 = _attributes[indexA].Get(1).Interpolate(_attributes[indexB].Get(1), ratio); + _attributes[indexB].attr1 = _attributes[indexA].attr1; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override MetaAttributeList AddAttributeType() + { + MetaAttributeList newAttributes = new MetaAttributeList(_attributes.Length); + for (int i = 0; i < Count; i++) + newAttributes.Set(new MetaAttribute(_attributes[i].attr0, _attributes[i].attr1, default(T)), i); + return newAttributes; + } + + public override int Count => _attributes.Length; + + public override int CountPerAttribute => 2; + } + + public class MetaAttributeList : MetaAttributeList + where T0 : unmanaged, IInterpolable + where T1 : unmanaged, IInterpolable + where T2 : unmanaged, IInterpolable + { + private readonly MetaAttribute[] _attributes; + + public MetaAttributeList(int length) + { + _attributes = new MetaAttribute[length]; + } + + public override IMetaAttribute this[int index] + { + get => _attributes[index]; + set => _attributes[index] = (MetaAttribute)value; + } + + public void Set(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + private void Get(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + public override MetaAttributeList CreateNew(int length) => new MetaAttributeList(length); + + public override unsafe bool Equals(int indexA, int indexB, int attribute) + { + switch (attribute) + { + case 0: + return _attributes[indexA].Get(0).Equals(_attributes[indexB].Get(0)); + case 1: + return _attributes[indexA].Get(1).Equals(_attributes[indexB].Get(1)); + case 2: + return _attributes[indexA].Get(2).Equals(_attributes[indexB].Get(2)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override void Interpolate(int attribute, int indexA, int indexB, double ratio) + { + switch (attribute) + { + case 0: + _attributes[indexA].attr0 = _attributes[indexA].Get(0).Interpolate(_attributes[indexB].Get(0), ratio); + _attributes[indexB].attr0 = _attributes[indexA].attr0; + break; + case 1: + _attributes[indexA].attr1 = _attributes[indexA].Get(1).Interpolate(_attributes[indexB].Get(1), ratio); + _attributes[indexB].attr1 = _attributes[indexA].attr1; + break; + case 2: + _attributes[indexA].attr2 = _attributes[indexA].Get(2).Interpolate(_attributes[indexB].Get(2), ratio); + _attributes[indexB].attr2 = _attributes[indexA].attr2; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override MetaAttributeList AddAttributeType() + { + MetaAttributeList newAttributes = new MetaAttributeList(_attributes.Length); + for (int i = 0; i < Count; i++) + newAttributes.Set(new MetaAttribute(_attributes[i].attr0, _attributes[i].attr1, _attributes[i].attr2, default(T)), i); + return newAttributes; + } + + public override int Count => _attributes.Length; + + public override int CountPerAttribute => 3; + } + + public class MetaAttributeList : MetaAttributeList + where T0 : unmanaged, IInterpolable + where T1 : unmanaged, IInterpolable + where T2 : unmanaged, IInterpolable + where T3 : unmanaged, IInterpolable + { + private readonly MetaAttribute[] _attributes; + + public MetaAttributeList(int length) + { + _attributes = new MetaAttribute[length]; + } + + public override IMetaAttribute this[int index] + { + get => _attributes[index]; + set => _attributes[index] = (MetaAttribute)value; + } + + public void Set(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + private void Get(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + public override MetaAttributeList CreateNew(int length) => new MetaAttributeList(length); + + public override unsafe bool Equals(int indexA, int indexB, int attribute) + { + switch (attribute) + { + case 0: + return _attributes[indexA].Get(0).Equals(_attributes[indexB].Get(0)); + case 1: + return _attributes[indexA].Get(1).Equals(_attributes[indexB].Get(1)); + case 2: + return _attributes[indexA].Get(2).Equals(_attributes[indexB].Get(2)); + case 3: + return _attributes[indexA].Get(3).Equals(_attributes[indexB].Get(3)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override void Interpolate(int attribute, int indexA, int indexB, double ratio) + { + switch (attribute) + { + case 0: + _attributes[indexA].attr0 = _attributes[indexA].Get(0).Interpolate(_attributes[indexB].Get(0), ratio); + _attributes[indexB].attr0 = _attributes[indexA].attr0; + break; + case 1: + _attributes[indexA].attr1 = _attributes[indexA].Get(1).Interpolate(_attributes[indexB].Get(1), ratio); + _attributes[indexB].attr1 = _attributes[indexA].attr1; + break; + case 2: + _attributes[indexA].attr2 = _attributes[indexA].Get(2).Interpolate(_attributes[indexB].Get(2), ratio); + _attributes[indexB].attr2 = _attributes[indexA].attr2; + break; + case 3: + _attributes[indexA].attr3 = _attributes[indexA].Get(3).Interpolate(_attributes[indexB].Get(3), ratio); + _attributes[indexB].attr3 = _attributes[indexA].attr3; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override MetaAttributeList AddAttributeType() + { + MetaAttributeList newAttributes = new MetaAttributeList(_attributes.Length); + for (int i = 0; i < Count; i++) + newAttributes.Set(new MetaAttribute(_attributes[i].attr0, _attributes[i].attr1, _attributes[i].attr2, _attributes[i].attr3, default(T)), i); + return newAttributes; + } + + public override int Count => _attributes.Length; + + public override int CountPerAttribute => 4; + } + + public class MetaAttributeList : MetaAttributeList + where T0 : unmanaged, IInterpolable + where T1 : unmanaged, IInterpolable + where T2 : unmanaged, IInterpolable + where T3 : unmanaged, IInterpolable + where T4 : unmanaged, IInterpolable + { + private readonly MetaAttribute[] _attributes; + + public MetaAttributeList(int length) + { + _attributes = new MetaAttribute[length]; + } + + public override IMetaAttribute this[int index] + { + get => _attributes[index]; + set => _attributes[index] = (MetaAttribute)value; + } + + public void Set(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + private void Get(MetaAttribute value, int index) + { + _attributes[index] = value; + } + + public override MetaAttributeList CreateNew(int length) => new MetaAttributeList(length); + + public override unsafe bool Equals(int indexA, int indexB, int attribute) + { + switch (attribute) + { + case 0: + return _attributes[indexA].Get(0).Equals(_attributes[indexB].Get(0)); + case 1: + return _attributes[indexA].Get(1).Equals(_attributes[indexB].Get(1)); + case 2: + return _attributes[indexA].Get(2).Equals(_attributes[indexB].Get(2)); + case 3: + return _attributes[indexA].Get(3).Equals(_attributes[indexB].Get(3)); + case 4: + return _attributes[indexA].Get(3).Equals(_attributes[indexB].Get(4)); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override void Interpolate(int attribute, int indexA, int indexB, double ratio) + { + switch (attribute) + { + case 0: + _attributes[indexA].attr0 = _attributes[indexA].Get(0).Interpolate(_attributes[indexB].Get(0), ratio); + _attributes[indexB].attr0 = _attributes[indexA].attr0; + break; + case 1: + _attributes[indexA].attr1 = _attributes[indexA].Get(1).Interpolate(_attributes[indexB].Get(1), ratio); + _attributes[indexB].attr1 = _attributes[indexA].attr1; + break; + case 2: + _attributes[indexA].attr2 = _attributes[indexA].Get(2).Interpolate(_attributes[indexB].Get(2), ratio); + _attributes[indexB].attr2 = _attributes[indexA].attr2; + break; + case 3: + _attributes[indexA].attr3 = _attributes[indexA].Get(3).Interpolate(_attributes[indexB].Get(3), ratio); + _attributes[indexB].attr3 = _attributes[indexA].attr3; + break; + case 4: + _attributes[indexA].attr4 = _attributes[indexA].Get(4).Interpolate(_attributes[indexB].Get(4), ratio); + _attributes[indexB].attr4 = _attributes[indexA].attr4; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override MetaAttributeList AddAttributeType() + { + throw new NotImplementedException(); + } + + public override int Count => _attributes.Length; + + public override int CountPerAttribute => 5; + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/ConnectedMesh.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/ConnectedMesh.cs new file mode 100644 index 0000000..2cb81c8 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/ConnectedMesh.cs @@ -0,0 +1,706 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Nanomesh +{ + // Let's say F = 2V + // Halfedge mesh is V * sizeof(vertex) + 3F * sizeof(Halfedge) + F * sizeof(Face) = 16 * 0.5F + 3F * 20 + 4F = 72F + // Connected mesh is V * sizeof(Vector3) + 3F * sizeof(Node) + F * sizeof(Face) = 12 * 0.5F + 3F * 12 + 12F = 54F (without attributes) + // Connected mesh no face is V * sizeof(Vector3) + 3F * sizeof(Node) = 12 * 0.5F + 3F * 12 = 42F (without attributes) + public partial class ConnectedMesh + { + // Todo : make this private (can only be modified from the inside) + public Vector3[] positions; + public MetaAttributeList attributes; + public Node[] nodes; + public Group[] groups; + public AttributeDefinition[] attributeDefinitions; + + public int[] PositionToNode => _positionToNode ?? (_positionToNode = GetPositionToNode()); + private int[] _positionToNode; + + internal int _faceCount; + public int FaceCount => _faceCount; + + public bool AreNodesSiblings(int nodeIndexA, int nodeIndexB) + { + return nodes[nodeIndexA].position == nodes[nodeIndexB].position; + } + + public int[] GetPositionToNode() + { + int[] positionToNode = new int[positions.Length]; + + for (int i = 0; i < positions.Length; i++) + { + positionToNode[i] = -1; + } + + for (int i = 0; i < nodes.Length; i++) + { + if (!nodes[i].IsRemoved) + { + positionToNode[nodes[i].position] = i; + } + } + + return positionToNode; + } + + public int GetEdgeCount(int nodeIndex) + { + return GetRelativesCount(nodeIndex) + 1; + } + + public int GetRelativesCount(int nodeIndex) + { + int k = 0; + int relative = nodeIndex; + while ((relative = nodes[relative].relative) != nodeIndex) + { + k++; + } + return k; + } + + public int GetSiblingsCount(int nodeIndex) + { + int k = 0; + int sibling = nodeIndex; + while ((sibling = nodes[sibling].sibling) != nodeIndex) + { + k++; + } + return k; + } + + public int ReconnectSiblings(int nodeIndex) + { + int sibling = nodeIndex; + int lastValid = -1; + int firstValid = -1; + int position = -1; + + do + { + if (nodes[sibling].IsRemoved) + { + continue; + } + + if (firstValid == -1) + { + firstValid = sibling; + position = nodes[sibling].position; + } + + if (lastValid != -1) + { + nodes[lastValid].sibling = sibling; + nodes[lastValid].position = position; + } + + lastValid = sibling; + } + while ((sibling = nodes[sibling].sibling) != nodeIndex); + + if (lastValid == -1) + { + return -1; // All siblings were removed + } + + // Close the loop + nodes[lastValid].sibling = firstValid; + nodes[lastValid].position = position; + + return firstValid; + } + + public int ReconnectSiblings(int nodeIndexA, int nodeIndexB, int position) + { + int sibling = nodeIndexA; + int lastValid = -1; + int firstValid = -1; + + do + { + if (nodes[sibling].IsRemoved) + { + continue; + } + + if (firstValid == -1) + { + firstValid = sibling; + //position = nodes[sibling].position; + } + + if (lastValid != -1) + { + nodes[lastValid].sibling = sibling; + nodes[lastValid].position = position; + } + + lastValid = sibling; + } + while ((sibling = nodes[sibling].sibling) != nodeIndexA); + + sibling = nodeIndexB; + do + { + if (nodes[sibling].IsRemoved) + { + continue; + } + + if (firstValid == -1) + { + firstValid = sibling; + //position = nodes[sibling].position; + } + + if (lastValid != -1) + { + nodes[lastValid].sibling = sibling; + nodes[lastValid].position = position; + } + + lastValid = sibling; + } + while ((sibling = nodes[sibling].sibling) != nodeIndexB); + + if (lastValid == -1) + { + return -1; // All siblings were removed + } + + // Close the loop + nodes[lastValid].sibling = firstValid; + nodes[lastValid].position = position; + + return firstValid; + } + + public int CollapseEdge(int nodeIndexA, int nodeIndexB) + { + int posA = nodes[nodeIndexA].position; + int posB = nodes[nodeIndexB].position; + + Debug.Assert(posA != posB, "A and B must have different positions"); + Debug.Assert(!nodes[nodeIndexA].IsRemoved); + Debug.Assert(!nodes[nodeIndexB].IsRemoved); + + Debug.Assert(CheckRelatives(nodeIndexA), "A's relatives must be valid"); + Debug.Assert(CheckRelatives(nodeIndexB), "B's relatives must be valid"); + Debug.Assert(CheckSiblings(nodeIndexA), "A's siblings must be valid"); + Debug.Assert(CheckSiblings(nodeIndexB), "B's siblings must be valid"); + + int siblingOfA = nodeIndexA; + do // Iterates over faces around A + { + bool isFaceTouched = false; + int faceEdgeCount = 0; + int nodeIndexC = -1; + + int relativeOfA = siblingOfA; + do // Circulate in face + { + int posC = nodes[relativeOfA].position; + if (posC == posB) + { + isFaceTouched = true; + } + else if (posC != posA) + { + nodeIndexC = relativeOfA; + } + + faceEdgeCount++; + } while ((relativeOfA = nodes[relativeOfA].relative) != siblingOfA); + + if (faceEdgeCount != 3) + throw new NotImplementedException(); + + if (isFaceTouched && faceEdgeCount == 3) + { + // Remove face : Mark nodes as removed an reconnect siblings around C + + int posC = nodes[nodeIndexC].position; + + relativeOfA = siblingOfA; + do + { + nodes[relativeOfA].MarkRemoved(); + + } while ((relativeOfA = nodes[relativeOfA].relative) != siblingOfA); + + int validNodeAtC = ReconnectSiblings(nodeIndexC); + + if (_positionToNode != null) + { + _positionToNode[posC] = validNodeAtC; + } + + _faceCount--; + } + } while ((siblingOfA = nodes[siblingOfA].sibling) != nodeIndexA); + + int validNodeAtA = ReconnectSiblings(nodeIndexA, nodeIndexB, posA); + + if (_positionToNode != null) + { + _positionToNode[posA] = validNodeAtA; + _positionToNode[posB] = -1; + } + + return validNodeAtA; + } + + public double GetEdgeTopo(int nodeIndexA, int nodeIndexB) + { + if ((uint)nodeIndexA >= (uint)nodes.Length || (uint)nodeIndexB >= (uint)nodes.Length) + { + return EdgeBorderPenalty; + } + + if (nodes[nodeIndexA].IsRemoved || nodes[nodeIndexB].IsRemoved) + { + return EdgeBorderPenalty; + } + + int posB = nodes[nodeIndexB].position; + + int facesAttached = 0; + + int attrAtA = -1; + int attrAtB = -1; + + double edgeWeight = 0; + + int siblingOfA = nodeIndexA; + do + { + int relativeOfA = siblingOfA; + while ((relativeOfA = nodes[relativeOfA].relative) != siblingOfA) + { + int posC = nodes[relativeOfA].position; + if (posC == posB) + { + facesAttached++; + + if (attributes != null) + { + for (int i = 0; i < attributes.CountPerAttribute; i++) + { + if (attrAtB != -1 && !attributes.Equals(attrAtB, nodes[relativeOfA].attribute, i)) + { + edgeWeight += attributeDefinitions[i].weight; + } + + if (attrAtA != -1 && !attributes.Equals(attrAtA, nodes[siblingOfA].attribute, i)) + { + edgeWeight += attributeDefinitions[i].weight; + } + } + } + + attrAtB = nodes[relativeOfA].attribute; + attrAtA = nodes[siblingOfA].attribute; + } + } + } while ((siblingOfA = nodes[siblingOfA].sibling) != nodeIndexA); + + if (facesAttached != 2) // Border or non-manifold edge + { + edgeWeight += EdgeBorderPenalty; + } + + return edgeWeight; + } + + internal static double EdgeBorderPenalty = 1027.007; + + // TODO : Make it work with any polygon (other than triangle) + public Vector3 GetFaceNormal(int nodeIndex) + { + int posA = nodes[nodeIndex].position; + int posB = nodes[nodes[nodeIndex].relative].position; + int posC = nodes[nodes[nodes[nodeIndex].relative].relative].position; + + Vector3 normal = Vector3.Cross( + positions[posB] - positions[posA], + positions[posC] - positions[posA]); + + return normal.Normalized; + } + + // TODO : Make it work with any polygon (other than triangle) + public double GetFaceArea(int nodeIndex) + { + int posA = nodes[nodeIndex].position; + int posB = nodes[nodes[nodeIndex].relative].position; + int posC = nodes[nodes[nodes[nodeIndex].relative].relative].position; + + Vector3 normal = Vector3.Cross( + positions[posB] - positions[posA], + positions[posC] - positions[posA]); + + return 0.5 * normal.Length; + } + + // Only works with triangles ! + public double GetAngleRadians(int nodeIndex) + { + int posA = nodes[nodeIndex].position; + int posB = nodes[nodes[nodeIndex].relative].position; + int posC = nodes[nodes[nodes[nodeIndex].relative].relative].position; + + return Vector3.AngleRadians( + positions[posB] - positions[posA], + positions[posC] - positions[posA]); + } + + public void Compact() + { + // Rebuild nodes array with only valid nodes + { + int validNodesCount = 0; + for (int i = 0; i < nodes.Length; i++) + if (!nodes[i].IsRemoved) + validNodesCount++; + + Node[] newNodes = new Node[validNodesCount]; + int k = 0; + Dictionary oldToNewNodeIndex = new Dictionary(); + for (int i = 0; i < nodes.Length; i++) + { + if (!nodes[i].IsRemoved) + { + newNodes[k] = nodes[i]; + oldToNewNodeIndex.Add(i, k); + k++; + } + } + for (int i = 0; i < newNodes.Length; i++) + { + newNodes[i].relative = oldToNewNodeIndex[newNodes[i].relative]; + newNodes[i].sibling = oldToNewNodeIndex[newNodes[i].sibling]; + } + nodes = newNodes; + } + + // Remap positions + { + Dictionary oldToNewPosIndex = new Dictionary(); + for (int i = 0; i < nodes.Length; i++) + { + if (!oldToNewPosIndex.ContainsKey(nodes[i].position)) + oldToNewPosIndex.Add(nodes[i].position, oldToNewPosIndex.Count); + + nodes[i].position = oldToNewPosIndex[nodes[i].position]; + } + Vector3[] newPositions = new Vector3[oldToNewPosIndex.Count]; + foreach (KeyValuePair oldToNewPos in oldToNewPosIndex) + { + newPositions[oldToNewPos.Value] = positions[oldToNewPos.Key]; + } + positions = newPositions; + } + + // Remap attributes + if (attributes != null) + { + Dictionary oldToNewAttrIndex = new Dictionary(); + for (int i = 0; i < nodes.Length; i++) + { + if (!oldToNewAttrIndex.ContainsKey(nodes[i].attribute)) + oldToNewAttrIndex.Add(nodes[i].attribute, oldToNewAttrIndex.Count); + + nodes[i].attribute = oldToNewAttrIndex[nodes[i].attribute]; + } + MetaAttributeList newAttributes = attributes.CreateNew(oldToNewAttrIndex.Count); + foreach (KeyValuePair oldToNewAttr in oldToNewAttrIndex) + { + newAttributes[oldToNewAttr.Value] = attributes[oldToNewAttr.Key]; + } + attributes = newAttributes; + } + + _positionToNode = null; // Invalid now + } + + public void MergePositions(double tolerance = 0.01) + { + Dictionary newPositions = new Dictionary(tolerance <= 0 ? null : new Vector3Comparer(tolerance)); + + for (int i = 0; i < positions.Length; i++) + { + newPositions.TryAdd(positions[i], newPositions.Count); + } + + for (int i = 0; i < nodes.Length; i++) + { + nodes[i].position = newPositions[positions[nodes[i].position]]; + } + + positions = new Vector3[newPositions.Count]; + foreach (KeyValuePair pair in newPositions) + { + positions[pair.Value] = pair.Key; + } + + newPositions = null; + + // Remapping siblings + Dictionary posToLastSibling = new Dictionary(); + + for (int i = 0; i < nodes.Length; i++) + { + if (posToLastSibling.ContainsKey(nodes[i].position)) + { + nodes[i].sibling = posToLastSibling[nodes[i].position]; + posToLastSibling[nodes[i].position] = i; + } + else + { + nodes[i].sibling = -1; + posToLastSibling.Add(nodes[i].position, i); + } + } + + for (int i = 0; i < nodes.Length; i++) + { + if (nodes[i].sibling < 0) + { + // Assign last sibling to close sibling loop + nodes[i].sibling = posToLastSibling[nodes[i].position]; + } + } + + _positionToNode = null; + + // Dereference faces that no longer exist + for (int i = 0; i < nodes.Length; i++) + { + if (nodes[i].IsRemoved) + { + continue; + } + + int lastPos = nodes[i].position; + int relative = i; + while ((relative = nodes[relative].relative) != i) // Circulate around face + { + int currPos = nodes[relative].position; + if (lastPos == currPos) + { + RemoveFace(relative); + break; + } + lastPos = currPos; + } + } + } + + public void MergeAttributes() + { + Dictionary _uniqueAttributes = new Dictionary(); + + for (int i = 0; i < nodes.Length; i++) + { + _uniqueAttributes.TryAdd(attributes[nodes[i].attribute], nodes[i].attribute); + } + + for (int i = 0; i < nodes.Length; i++) + { + nodes[i].attribute = _uniqueAttributes[attributes[nodes[i].attribute]]; + } + } + + public void RemoveFace(int nodeIndex) + { + int relative = nodeIndex; + do + { + nodes[relative].MarkRemoved(); + ReconnectSiblings(relative); + } while ((relative = nodes[relative].relative) != nodeIndex); + _faceCount--; + } + + public void Scale(double factor) + { + for (int i = 0; i < positions.Length; i++) + { + positions[i] = positions[i] * factor; + } + } + + public HashSet GetAllEdges() + { + HashSet edges = new HashSet(); + for (int p = 0; p < PositionToNode.Length; p++) + { + int nodeIndex = PositionToNode[p]; + if (nodeIndex < 0) + { + continue; + } + + int sibling = nodeIndex; + do + { + int firstRelative = nodes[sibling].relative; + int secondRelative = nodes[firstRelative].relative; + + Edge pair = new Edge(nodes[firstRelative].position, nodes[secondRelative].position); + + edges.Add(pair); + + } while ((sibling = nodes[sibling].sibling) != nodeIndex); + } + + return edges; + } + + public SharedMesh ToSharedMesh() + { + // Compating here is an issue if mesh is being decimated :/ + //Compact(); + + SharedMesh mesh = new SharedMesh(); + + List triangles = new List(); + HashSet browsedNodes = new HashSet(); + + Group[] newGroups = new Group[groups?.Length ?? 0]; + mesh.groups = newGroups; + mesh.attributeDefinitions = attributeDefinitions; + + int currentGroup = 0; + int indicesInGroup = 0; + + Dictionary<(int, int), int> perVertexMap = new Dictionary<(int, int), int>(); + + for (int i = 0; i < nodes.Length; i++) + { + if (newGroups.Length > 0 && groups[currentGroup].firstIndex == i) + { + if (currentGroup > 0) + { + newGroups[currentGroup - 1].indexCount = indicesInGroup; + newGroups[currentGroup].firstIndex = indicesInGroup + newGroups[currentGroup - 1].firstIndex; + } + indicesInGroup = 0; + if (currentGroup < groups.Length - 1) + { + currentGroup++; + } + } + + if (nodes[i].IsRemoved) + { + continue; + } + + indicesInGroup++; + + if (browsedNodes.Contains(i)) + { + continue; + } + + // Only works if all elements are triangles + int relative = i; + do + { + if (browsedNodes.Add(relative) && !nodes[relative].IsRemoved) + { + (int position, int attribute) key = (nodes[relative].position, nodes[relative].attribute); + perVertexMap.TryAdd(key, perVertexMap.Count); + triangles.Add(perVertexMap[key]); + } + } while ((relative = nodes[relative].relative) != i); + } + + if (newGroups.Length > 0) + { + newGroups[currentGroup].indexCount = indicesInGroup; + } + + // Positions + mesh.positions = new Vector3[perVertexMap.Count]; + foreach (KeyValuePair<(int, int), int> mapping in perVertexMap) + { + mesh.positions[mapping.Value] = positions[mapping.Key.Item1]; + } + + // Attributes + if (attributes != null && attributeDefinitions.Length > 0) + { + mesh.attributes = attributes.CreateNew(perVertexMap.Count); + foreach (KeyValuePair<(int, int), int> mapping in perVertexMap) + { + mesh.attributes[mapping.Value] = attributes[mapping.Key.Item2]; + } + } + + mesh.triangles = triangles.ToArray(); + + return mesh; + } + } + + public struct Edge : IEquatable + { + public int posA; + public int posB; + + public Edge(int posA, int posB) + { + this.posA = posA; + this.posB = posB; + } + + public override int GetHashCode() + { + unchecked + { + return posA + posB; + } + } + + public override bool Equals(object obj) + { + return Equals((Edge)obj); + } + + public bool Equals(Edge pc) + { + if (ReferenceEquals(this, pc)) + { + return true; + } + else + { + return (posA == pc.posA && posB == pc.posB) || (posA == pc.posB && posB == pc.posA); + } + } + + public static bool operator ==(Edge x, Edge y) + { + return x.Equals(y); + } + + public static bool operator !=(Edge x, Edge y) + { + return !x.Equals(y); + } + + public override string ToString() + { + return $""; + } + } +} diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/Debug.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/Debug.cs new file mode 100644 index 0000000..818cb62 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/Debug.cs @@ -0,0 +1,138 @@ +using System; +using System.Linq; + +namespace Nanomesh +{ + public partial class ConnectedMesh + { + internal string PrintSiblings(int nodeIndex) + { + int sibling = nodeIndex; + string text = string.Join(" > ", Enumerable.Range(0, 12).Select(x => + { + string res = sibling.ToString() + (nodes[sibling].IsRemoved ? "(x)" : $"({nodes[sibling].position})"); + sibling = nodes[sibling].sibling; + return res; + })); + return text + "..."; + } + + internal string PrintRelatives(int nodeIndex) + { + int relative = nodeIndex; + string text = string.Join(" > ", Enumerable.Range(0, 12).Select(x => + { + string res = relative.ToString() + (nodes[relative].IsRemoved ? "(x)" : $"({nodes[relative].position})"); + relative = nodes[relative].relative; + return res; + })); + return text + "..."; + } + + internal bool CheckEdge(int nodeIndexA, int nodeIndexB) + { + if (nodes[nodeIndexA].position == nodes[nodeIndexB].position) + { + throw new Exception("Positions must be different"); + } + + if (nodes[nodeIndexA].IsRemoved) + { + throw new Exception($"Node A is unreferenced {nodeIndexA}"); + } + + if (nodes[nodeIndexB].IsRemoved) + { + throw new Exception($"Node B is unreferenced {nodeIndexB}"); + } + + return true; + } + + internal bool CheckRelatives(int nodeIndex) + { + if (nodes[nodeIndex].IsRemoved) + { + throw new Exception($"Node {nodeIndex} is removed"); + } + + int relative = nodeIndex; + int edgecount = 0; + int prevPos = -2; + do + { + if (nodes[relative].position == prevPos) + { + throw new Exception($"Two relatives or more share the same position : {PrintRelatives(nodeIndex)}"); + } + + if (edgecount > 50) + { + throw new Exception($"Circularity relative violation : {PrintRelatives(nodeIndex)}"); + } + + if (nodes[relative].IsRemoved) + { + throw new Exception($"Node {nodeIndex} is connected to the deleted relative {relative}"); + } + + prevPos = nodes[relative].position; + edgecount++; + + } while ((relative = nodes[relative].relative) != nodeIndex); + + return true; + } + + internal bool CheckSiblings(int nodeIndex) + { + if (nodes[nodeIndex].IsRemoved) + { + throw new Exception($"Node {nodeIndex} is removed"); + } + + int sibling = nodeIndex; + int cardinality = 0; + do + { + if (cardinality > 1000) + { + //throw new Exception($"Node {i}'s cardinality is superior to 50. It is likely to be that face siblings are not circularily linked"); + throw new Exception($"Circularity sibling violation : {PrintSiblings(nodeIndex)}"); + } + + if (nodes[sibling].IsRemoved) + { + throw new Exception($"Node {nodeIndex} has a deleted sibling {sibling}"); + } + + cardinality++; + + } while ((sibling = nodes[sibling].sibling) != nodeIndex); + + return true; + } + + internal bool Check() + { + for (int nodeIndex = 0; nodeIndex < nodes.Length; nodeIndex++) + { + if (nodes[nodeIndex].IsRemoved) + { + continue; + } + + CheckRelatives(nodeIndex); + + CheckSiblings(nodeIndex); + + if (GetEdgeCount(nodeIndex) == 2) + { + throw new Exception($"Node {nodeIndex} is part of a polygon of degree 2"); + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/Node.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/Node.cs new file mode 100644 index 0000000..1e92240 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/Node.cs @@ -0,0 +1,25 @@ +namespace Nanomesh +{ + public partial class ConnectedMesh + { + public struct Node + { + public int position; + public int sibling; + public int relative; + public int attribute; + + public void MarkRemoved() + { + position = -10; + } + + public bool IsRemoved => position == -10; + + public override string ToString() + { + return $"sibl:{sibling} rela:{relative} posi:{position}"; + } + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/Group.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/Group.cs new file mode 100644 index 0000000..87dab9c --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/Group.cs @@ -0,0 +1,8 @@ +namespace Nanomesh +{ + public struct Group + { + public int firstIndex; + public int indexCount; + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Mesh/SharedMesh.cs b/LightlessSync/ThirdParty/Nanomesh/Mesh/SharedMesh.cs new file mode 100644 index 0000000..eef5c31 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Mesh/SharedMesh.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Nanomesh +{ + /// + /// A shared mesh is a flattened approach of the triangle mesh. + /// Is does not has connectivity information, but it is simple to create + /// and is a rather lightweight mesh data structure. + /// + public class SharedMesh + { + public Vector3[] positions; + public int[] triangles; + public Group[] groups; + public MetaAttributeList attributes; + public AttributeDefinition[] attributeDefinitions; + + [Conditional("DEBUG")] + public void CheckLengths() + { + //if (attributes != null) + //{ + // foreach (var pair in attributes) + // { + // Debug.Assert(pair.Value.Length == vertices.Length, $"Attribute '{pair.Value}' must have as many elements as vertices"); + // } + //} + } + + public ConnectedMesh ToConnectedMesh() + { + CheckLengths(); + + ConnectedMesh connectedMesh = new ConnectedMesh + { + groups = groups + }; + + connectedMesh.positions = positions; + connectedMesh.attributes = attributes; + connectedMesh.attributeDefinitions = attributeDefinitions; + + // Building relatives + ConnectedMesh.Node[] nodes = new ConnectedMesh.Node[triangles.Length]; + Dictionary> vertexToNodes = new Dictionary>(); + for (int i = 0; i < triangles.Length; i += 3) + { + ConnectedMesh.Node A = new ConnectedMesh.Node(); + ConnectedMesh.Node B = new ConnectedMesh.Node(); + ConnectedMesh.Node C = new ConnectedMesh.Node(); + + A.position = triangles[i]; + B.position = triangles[i + 1]; + C.position = triangles[i + 2]; + + A.attribute = triangles[i]; + B.attribute = triangles[i + 1]; + C.attribute = triangles[i + 2]; + + A.relative = i + 1; // B + B.relative = i + 2; // C + C.relative = i; // A + + if (!vertexToNodes.ContainsKey(A.position)) + { + vertexToNodes.Add(A.position, new List()); + } + + if (!vertexToNodes.ContainsKey(B.position)) + { + vertexToNodes.Add(B.position, new List()); + } + + if (!vertexToNodes.ContainsKey(C.position)) + { + vertexToNodes.Add(C.position, new List()); + } + + vertexToNodes[A.position].Add(i); + vertexToNodes[B.position].Add(i + 1); + vertexToNodes[C.position].Add(i + 2); + + nodes[i] = A; + nodes[i + 1] = B; + nodes[i + 2] = C; + + connectedMesh._faceCount++; + } + + // Building siblings + foreach (KeyValuePair> pair in vertexToNodes) + { + int previousSibling = -1; + int firstSibling = -1; + foreach (int node in pair.Value) + { + if (firstSibling != -1) + { + nodes[node].sibling = previousSibling; + } + else + { + firstSibling = node; + } + previousSibling = node; + } + nodes[firstSibling].sibling = previousSibling; + } + + connectedMesh.nodes = nodes; + + Debug.Assert(connectedMesh.Check()); + + return connectedMesh; + } + } +} \ No newline at end of file diff --git a/LightlessSync/ThirdParty/Nanomesh/Todo.md b/LightlessSync/ThirdParty/Nanomesh/Todo.md new file mode 100644 index 0000000..b6312f1 --- /dev/null +++ b/LightlessSync/ThirdParty/Nanomesh/Todo.md @@ -0,0 +1,22 @@ +# Todo List NOT LIGHTLESS RELATED XD + +- [x] Bench iterating methods +- [x] Add a bunch of primitives +- [x] Add ConnectedMesh data structure +- [x] Add SharedMesh data structure + - [ ] Add vertex attributes +- [x] Add SharedMesh -> ConnectedMesh + - [ ] Add support for hardedges + - [ ] Add conversion of attributes +- [x] Add ConnectedMesh -> SharedMesh + - [ ] Add support for hardedges +- [x] Add export to obj + - [ ] Add support for normals +- [x] Add import from obj + - [ ] Add support for normals +- [x] Add decimate + - [x] Optimize until it is satisfying + - [ ] Take into account vertex normals + - [ ] Take into account borders + - [ ] Add an error target control +- [ ] Add create normals function \ No newline at end of file diff --git a/LightlessSync/UI/Components/OptimizationSettingsPanel.cs b/LightlessSync/UI/Components/OptimizationSettingsPanel.cs index 7b0477f..d8b8bd1 100644 --- a/LightlessSync/UI/Components/OptimizationSettingsPanel.cs +++ b/LightlessSync/UI/Components/OptimizationSettingsPanel.cs @@ -299,101 +299,12 @@ public sealed class OptimizationSettingsPanel DrawGroupHeader("Core Controls", UIColors.Get("LightlessOrange")); var performanceConfig = _performanceConfigService.Current; - using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) - using (var table = ImRaii.Table("model-opt-core", 3, SettingsTableFlags)) - { - if (table) - { - ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); - ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - - DrawControlRow("Enable model decimation", () => - { - var enableDecimation = performanceConfig.EnableModelDecimation; - var accent = UIColors.Get("LightlessOrange"); - if (DrawAccentCheckbox("##enable-model-decimation", ref enableDecimation, accent)) - { - performanceConfig.EnableModelDecimation = enableDecimation; - _performanceConfigService.Save(); - } - }, "Generates a decimated copy of models after download.", UIColors.Get("LightlessOrange"), UIColors.Get("LightlessOrange")); - - DrawControlRow("Decimate above (triangles)", () => - { - var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; - ImGui.SetNextItemWidth(-1f); - if (ImGui.SliderInt("##model-decimation-threshold", ref triangleThreshold, 1_000, 100_000)) - { - performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 1_000, 100_000); - _performanceConfigService.Save(); - } - }, "Models below this triangle count are left untouched. Default: 15,000."); - - DrawControlRow("Target triangle ratio", () => - { - var targetPercent = (float)(performanceConfig.ModelDecimationTargetRatio * 100.0); - var clampedPercent = Math.Clamp(targetPercent, 60f, 99f); - if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon) - { - performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0; - _performanceConfigService.Save(); - targetPercent = clampedPercent; - } - - ImGui.SetNextItemWidth(-1f); - if (ImGui.SliderFloat("##model-decimation-target", ref targetPercent, 60f, 99f, "%.0f%%")) - { - performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.6f, 0.99f); - _performanceConfigService.Save(); - } - }, "Ratio relative to original triangle count (80% keeps 80%). Default: 80%."); - } - } + DrawModelDecimationCard(performanceConfig); ImGui.Dummy(new Vector2(0f, 2f * scale)); DrawGroupHeader("Behavior & Exceptions", UIColors.Get("LightlessOrange")); - using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) - using (var table = ImRaii.Table("model-opt-behavior-table", 3, SettingsTableFlags)) - { - if (table) - { - ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); - ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - - DrawControlRow("Normalize tangents", () => - { - var normalizeTangents = performanceConfig.ModelDecimationNormalizeTangents; - if (ImGui.Checkbox("##model-normalize-tangents", ref normalizeTangents)) - { - performanceConfig.ModelDecimationNormalizeTangents = normalizeTangents; - _performanceConfigService.Save(); - } - }, "Normalizes tangents to reduce shading artifacts."); - - DrawControlRow("Keep original model files", () => - { - var keepOriginalModels = performanceConfig.KeepOriginalModelFiles; - if (ImGui.Checkbox("##model-keep-original", ref keepOriginalModels)) - { - performanceConfig.KeepOriginalModelFiles = keepOriginalModels; - _performanceConfigService.Save(); - } - }, "Keeps the original model alongside the decimated copy."); - - DrawControlRow("Skip preferred/direct pairs", () => - { - var skipPreferredDecimation = performanceConfig.SkipModelDecimationForPreferredPairs; - if (ImGui.Checkbox("##model-skip-preferred", ref skipPreferredDecimation)) - { - performanceConfig.SkipModelDecimationForPreferredPairs = skipPreferredDecimation; - _performanceConfigService.Save(); - } - }, "Leaves models untouched for preferred/direct pairs."); - } - } + DrawModelBehaviorCard(performanceConfig); UiSharedService.ColorTextWrapped( "Note: Disabling \"Keep original model files\" prevents saved/effective triangle usage information.", @@ -436,6 +347,7 @@ public sealed class OptimizationSettingsPanel ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + const string bodyDesc = "Body meshes (torso, limbs)."; DrawControlRow("Body", () => { var allowBody = config.ModelDecimationAllowBody; @@ -444,8 +356,15 @@ public sealed class OptimizationSettingsPanel config.ModelDecimationAllowBody = allowBody; _performanceConfigService.Save(); } - }, "Body meshes (torso, limbs)."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.ModelDecimationAllowBody = ModelDecimationDefaults.AllowBody; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{bodyDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowBody ? "On" : "Off")})."); + }, bodyDesc); + const string faceDesc = "Face and head meshes."; DrawControlRow("Face/head", () => { var allowFaceHead = config.ModelDecimationAllowFaceHead; @@ -454,8 +373,15 @@ public sealed class OptimizationSettingsPanel config.ModelDecimationAllowFaceHead = allowFaceHead; _performanceConfigService.Save(); } - }, "Face and head meshes."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.ModelDecimationAllowFaceHead = ModelDecimationDefaults.AllowFaceHead; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{faceDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowFaceHead ? "On" : "Off")})."); + }, faceDesc); + const string tailDesc = "Tail, ear, and similar appendages."; DrawControlRow("Tails/Ears", () => { var allowTail = config.ModelDecimationAllowTail; @@ -464,8 +390,15 @@ public sealed class OptimizationSettingsPanel config.ModelDecimationAllowTail = allowTail; _performanceConfigService.Save(); } - }, "Tail, ear, and similar appendages."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.ModelDecimationAllowTail = ModelDecimationDefaults.AllowTail; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{tailDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowTail ? "On" : "Off")})."); + }, tailDesc); + const string clothingDesc = "Outfits, shoes, gloves, hats."; DrawControlRow("Clothing", () => { var allowClothing = config.ModelDecimationAllowClothing; @@ -474,8 +407,15 @@ public sealed class OptimizationSettingsPanel config.ModelDecimationAllowClothing = allowClothing; _performanceConfigService.Save(); } - }, "Outfits, shoes, gloves, hats."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.ModelDecimationAllowClothing = ModelDecimationDefaults.AllowClothing; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{clothingDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowClothing ? "On" : "Off")})."); + }, clothingDesc); + const string accessoryDesc = "Jewelry and small add-ons."; DrawControlRow("Accessories", () => { var allowAccessories = config.ModelDecimationAllowAccessories; @@ -484,7 +424,13 @@ public sealed class OptimizationSettingsPanel config.ModelDecimationAllowAccessories = allowAccessories; _performanceConfigService.Save(); } - }, "Jewelry and small add-ons."); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.ModelDecimationAllowAccessories = ModelDecimationDefaults.AllowAccessories; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{accessoryDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowAccessories ? "On" : "Off")})."); + }, accessoryDesc); } } @@ -592,6 +538,181 @@ public sealed class OptimizationSettingsPanel }); } + private void DrawModelDecimationCard(PlayerPerformanceConfig performanceConfig) + { + var scale = ImGuiHelpers.GlobalScale; + var accent = UIColors.Get("LightlessOrange"); + var bg = new Vector4(accent.X, accent.Y, accent.Z, 0.12f); + var border = new Vector4(accent.X, accent.Y, accent.Z, 0.32f); + const string enableDesc = "Generates a decimated copy of models after download."; + const string thresholdDesc = "Models below this triangle count are left untouched. Default: 15,000."; + const string ratioDesc = "Ratio relative to original triangle count (80% keeps 80%). Default: 80%."; + + DrawPanelBox("model-decimation-card", bg, border, 6f * scale, new Vector2(10f * scale, 6f * scale), () => + { + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + using (var table = ImRaii.Table("model-opt-core-card", 2, SettingsTableFlags)) + { + if (!table) + { + return; + } + + ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); + ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthStretch); + + DrawInlineDescriptionRow("Enable model decimation", () => + { + var enableDecimation = performanceConfig.EnableModelDecimation; + if (DrawAccentCheckbox("##enable-model-decimation", ref enableDecimation, accent)) + { + performanceConfig.EnableModelDecimation = enableDecimation; + _performanceConfigService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + performanceConfig.EnableModelDecimation = ModelDecimationDefaults.EnableAutoDecimation; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{enableDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.EnableAutoDecimation ? "On" : "Off")})."); + }, enableDesc); + + DrawInlineDescriptionRow("Decimate above (triangles)", () => + { + var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; + ImGui.SetNextItemWidth(220f * scale); + if (ImGui.SliderInt("##model-decimation-threshold", ref triangleThreshold, 1_000, 100_000)) + { + performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 1_000, 100_000); + _performanceConfigService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + performanceConfig.ModelDecimationTriangleThreshold = ModelDecimationDefaults.TriangleThreshold; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{thresholdDesc}\nRight-click to reset to default ({ModelDecimationDefaults.TriangleThreshold:N0})."); + }, thresholdDesc); + + DrawInlineDescriptionRow("Target triangle ratio", () => + { + var targetPercent = (float)(performanceConfig.ModelDecimationTargetRatio * 100.0); + var clampedPercent = Math.Clamp(targetPercent, 60f, 99f); + if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon) + { + performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0; + _performanceConfigService.Save(); + targetPercent = clampedPercent; + } + + ImGui.SetNextItemWidth(220f * scale); + if (ImGui.SliderFloat("##model-decimation-target", ref targetPercent, 60f, 99f, "%.0f%%")) + { + performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.6f, 0.99f); + _performanceConfigService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + performanceConfig.ModelDecimationTargetRatio = ModelDecimationDefaults.TargetRatio; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{ratioDesc}\nRight-click to reset to default ({ModelDecimationDefaults.TargetRatio * 100:0}%)."); + }, ratioDesc); + } + }); + } + + private void DrawModelBehaviorCard(PlayerPerformanceConfig performanceConfig) + { + var scale = ImGuiHelpers.GlobalScale; + var baseColor = UIColors.Get("LightlessGrey"); + var bg = new Vector4(baseColor.X, baseColor.Y, baseColor.Z, 0.12f); + var border = new Vector4(baseColor.X, baseColor.Y, baseColor.Z, 0.32f); + const string normalizeDesc = "Normalizes tangents to reduce shading artifacts."; + const string avoidBodyDesc = "Uses body materials as a collision guard to reduce clothing clipping. Slower and may reduce decimation."; + const string keepOriginalDesc = "Keeps the original model alongside the decimated copy."; + const string skipPreferredDesc = "Leaves models untouched for preferred/direct pairs."; + + DrawPanelBox("model-behavior-card", bg, border, 6f * scale, new Vector2(10f * scale, 6f * scale), () => + { + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + using (var table = ImRaii.Table("model-opt-behavior-card", 2, SettingsTableFlags)) + { + if (!table) + { + return; + } + + ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); + ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthStretch); + + DrawInlineDescriptionRow("Normalize tangents", () => + { + var normalizeTangents = performanceConfig.ModelDecimationNormalizeTangents; + if (UiSharedService.CheckboxWithBorder("##model-normalize-tangents", ref normalizeTangents, baseColor)) + { + performanceConfig.ModelDecimationNormalizeTangents = normalizeTangents; + _performanceConfigService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + performanceConfig.ModelDecimationNormalizeTangents = ModelDecimationDefaults.NormalizeTangents; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{normalizeDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.NormalizeTangents ? "On" : "Off")})."); + }, normalizeDesc); + + DrawInlineDescriptionRow("Avoid body intersection", () => + { + var avoidBodyIntersection = performanceConfig.ModelDecimationAvoidBodyIntersection; + if (UiSharedService.CheckboxWithBorder("##model-body-collision", ref avoidBodyIntersection, baseColor)) + { + performanceConfig.ModelDecimationAvoidBodyIntersection = avoidBodyIntersection; + _performanceConfigService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + performanceConfig.ModelDecimationAvoidBodyIntersection = ModelDecimationDefaults.AvoidBodyIntersection; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{avoidBodyDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AvoidBodyIntersection ? "On" : "Off")})."); + }, avoidBodyDesc); + + DrawInlineDescriptionRow("Keep original model files", () => + { + var keepOriginalModels = performanceConfig.KeepOriginalModelFiles; + if (UiSharedService.CheckboxWithBorder("##model-keep-original", ref keepOriginalModels, baseColor)) + { + performanceConfig.KeepOriginalModelFiles = keepOriginalModels; + _performanceConfigService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + performanceConfig.KeepOriginalModelFiles = ModelDecimationDefaults.KeepOriginalModelFiles; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{keepOriginalDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.KeepOriginalModelFiles ? "On" : "Off")})."); + }, keepOriginalDesc); + + DrawInlineDescriptionRow("Skip preferred/direct pairs", () => + { + var skipPreferredDecimation = performanceConfig.SkipModelDecimationForPreferredPairs; + if (UiSharedService.CheckboxWithBorder("##model-skip-preferred", ref skipPreferredDecimation, baseColor)) + { + performanceConfig.SkipModelDecimationForPreferredPairs = skipPreferredDecimation; + _performanceConfigService.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + performanceConfig.SkipModelDecimationForPreferredPairs = ModelDecimationDefaults.SkipPreferredPairs; + _performanceConfigService.Save(); + } + UiSharedService.AttachToolTip($"{skipPreferredDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.SkipPreferredPairs ? "On" : "Off")})."); + }, skipPreferredDesc); + } + }); + } + private void DrawInlineDescriptionRow( string label, Action drawControl, diff --git a/LightlessSync/UI/Components/OptimizationSummaryCard.cs b/LightlessSync/UI/Components/OptimizationSummaryCard.cs index 62c0bc0..3ff4b23 100644 --- a/LightlessSync/UI/Components/OptimizationSummaryCard.cs +++ b/LightlessSync/UI/Components/OptimizationSummaryCard.cs @@ -535,6 +535,7 @@ public sealed class OptimizationSummaryCard new OptimizationTooltipLine("Triangle threshold", threshold), new OptimizationTooltipLine("Target ratio", targetRatio), new OptimizationTooltipLine("Normalize tangents", FormatOnOff(config.ModelDecimationNormalizeTangents), GetOnOffColor(config.ModelDecimationNormalizeTangents)), + new OptimizationTooltipLine("Avoid body intersection", FormatOnOff(config.ModelDecimationAvoidBodyIntersection), GetOnOffColor(config.ModelDecimationAvoidBodyIntersection)), new OptimizationTooltipLine("Keep original models", FormatOnOff(config.KeepOriginalModelFiles), GetOnOffColor(config.KeepOriginalModelFiles)), new OptimizationTooltipLine("Skip preferred pairs", FormatOnOff(config.SkipModelDecimationForPreferredPairs), GetOnOffColor(config.SkipModelDecimationForPreferredPairs)), new OptimizationTooltipLine("Targets", targetLabel, targetColor), diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index e0bfcb1..4ed7c30 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -8,13 +8,16 @@ using LightlessSync.API.Data.Enum; using LightlessSync.FileCache; using LightlessSync.Interop.Ipc; using LightlessSync.LightlessConfiguration; +using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ModelDecimation; using LightlessSync.Services.TextureCompression; using LightlessSync.UI.Models; using LightlessSync.Utils; using Microsoft.Extensions.Logging; using OtterTex; +using Penumbra.Api.Enums; using System.Buffers.Binary; using System.Globalization; using System.Numerics; @@ -34,9 +37,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private const float TextureFilterSplitterWidth = 8f; private const float TextureDetailSplitterWidth = 12f; private const float TextureDetailSplitterCollapsedWidth = 18f; + private const float ModelBatchSplitterHeight = 8f; private const float SelectedFilePanelLogicalHeight = 90f; private const float TextureHoverPreviewDelaySeconds = 1.75f; private const float TextureHoverPreviewSize = 350f; + private const float MinModelDetailPaneWidth = 520f; + private const float MaxModelDetailPaneWidth = 860f; private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f); private readonly CharacterAnalyzer _characterAnalyzer; @@ -47,12 +53,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly TransientResourceManager _transientResourceManager; private readonly TransientConfigService _transientConfigService; + private readonly ModelDecimationService _modelDecimationService; private readonly TextureCompressionService _textureCompressionService; private readonly TextureMetadataHelper _textureMetadataHelper; private readonly List _textureRows = new(); private readonly Dictionary _textureSelections = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _selectedModelKeys = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _texturePreviews = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _textureResolutionCache = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _textureWorkspaceTabs = new(); @@ -61,20 +69,25 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private Dictionary>? _cachedAnalysis; private CancellationTokenSource _conversionCancellationTokenSource = new(); + private CancellationTokenSource _modelDecimationCts = new(); private CancellationTokenSource _transientRecordCts = new(); private Task? _conversionTask; + private Task? _modelDecimationTask; private TextureConversionProgress? _lastConversionProgress; private float _textureFilterPaneWidth = 320f; private float _textureDetailPaneWidth = 360f; private float _textureDetailHeight = 360f; private float _texturePreviewSize = 360f; + private float _modelDetailPaneWidth = 720f; + private float _modelBatchPanelHeight = 0f; private string _conversionCurrentFileName = string.Empty; private string _selectedFileTypeTab = string.Empty; private string _selectedHash = string.Empty; private string _textureSearch = string.Empty; + private string _modelSearch = string.Empty; private string _textureSlotFilter = "All"; private string _selectedTextureKey = string.Empty; private string _selectedStoredCharacter = string.Empty; @@ -85,6 +98,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private int _conversionCurrentFileProgress = 0; private int _conversionTotalJobs; + private int _modelDecimationCurrentProgress = 0; + private int _modelDecimationTotalJobs = 0; private bool _hasUpdate = false; private bool _modalOpen = false; @@ -92,6 +107,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private bool _textureRowsDirty = true; private bool _textureDetailCollapsed = false; private bool _conversionFailed; + private bool _modelDecimationFailed; + private bool _showModelBatchAdvancedSettings; + private bool _dismissedModelBatchWarning; + private bool _modelBatchWarningNeverShowPending; + private bool _modelBatchWarningPendingInitialized; + private string _modelDecimationCurrentHash = string.Empty; private double _textureHoverStartTime = 0; #if DEBUG private bool _debugCompressionModalOpen = false; @@ -115,8 +136,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService, LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager, - TransientConfigService transientConfigService, TextureCompressionService textureCompressionService, - TextureMetadataHelper textureMetadataHelper) + TransientConfigService transientConfigService, ModelDecimationService modelDecimationService, + TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper) : base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService) { _characterAnalyzer = characterAnalyzer; @@ -126,6 +147,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _playerPerformanceConfig = playerPerformanceConfig; _transientResourceManager = transientResourceManager; _transientConfigService = transientConfigService; + _modelDecimationService = modelDecimationService; _textureCompressionService = textureCompressionService; _textureMetadataHelper = textureMetadataHelper; Mediator.Subscribe(this, (_) => @@ -428,6 +450,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _cachedAnalysis = CloneAnalysis(_characterAnalyzer.LastAnalysis); _hasUpdate = false; InvalidateTextureRows(); + PruneModelSelections(); } private void DrawContentTabs() @@ -943,8 +966,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _selectedFileTypeTab = string.Empty; } + var modelGroup = groupedfiles.FirstOrDefault(g => string.Equals(g.Key, "mdl", StringComparison.OrdinalIgnoreCase)); var otherFileGroups = groupedfiles - .Where(g => !string.Equals(g.Key, "tex", StringComparison.Ordinal)) + .Where(g => !string.Equals(g.Key, "tex", StringComparison.OrdinalIgnoreCase) + && !string.Equals(g.Key, "mdl", StringComparison.OrdinalIgnoreCase)) .ToList(); if (!string.IsNullOrEmpty(_selectedFileTypeTab) && @@ -958,7 +983,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _selectedFileTypeTab = otherFileGroups[0].Key; } - DrawTextureWorkspace(kvp.Key, otherFileGroups); + DrawTextureWorkspace(kvp.Key, modelGroup, otherFileGroups); } } } @@ -970,9 +995,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _selectedTextureKey = string.Empty; _selectedTextureKeys.Clear(); _textureSelections.Clear(); + _selectedModelKeys.Clear(); ResetTextureFilters(); InvalidateTextureRows(); _conversionFailed = false; + _modelDecimationFailed = false; #if DEBUG ResetDebugCompressionModalState(); #endif @@ -996,6 +1023,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _textureRowsBuildCts?.Cancel(); _textureRowsBuildCts?.Dispose(); _conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged; + _modelDecimationCts.CancelDispose(); } private void ConversionProgress_ProgressChanged(object? sender, TextureConversionProgress e) @@ -1097,6 +1125,22 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } + private void PruneModelSelections() + { + if (_cachedAnalysis == null || _selectedModelKeys.Count == 0) + { + return; + } + + var validKeys = _cachedAnalysis.Values + .SelectMany(entries => entries.Values) + .Where(entry => string.Equals(entry.FileType, "mdl", StringComparison.OrdinalIgnoreCase)) + .Select(entry => entry.Hash) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + _selectedModelKeys.RemoveWhere(key => !validKeys.Contains(key)); + } + private TextureRowBuildResult BuildTextureRows( Dictionary> analysis, CancellationToken token) @@ -1390,6 +1434,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private enum TextureWorkspaceTab { Textures, + Models, OtherFiles } @@ -1445,14 +1490,19 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase public ushort MipLevels { get; } } - private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList> otherFileGroups) + private void DrawTextureWorkspace( + ObjectKind objectKind, + IGrouping? modelGroup, + IReadOnlyList> otherFileGroups) { if (!_textureWorkspaceTabs.ContainsKey(objectKind)) { _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures; } - if (otherFileGroups.Count == 0) + var hasModels = modelGroup != null; + var hasOther = otherFileGroups.Count > 0; + if (!hasModels && !hasOther) { _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures; DrawTextureTabContent(objectKind); @@ -1473,8 +1523,22 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } - using (var otherFilesTab = ImRaii.TabItem($"Other file types###other_{objectKind}")) + if (hasModels && modelGroup != null) { + using var modelsTab = ImRaii.TabItem($"Models###models_{objectKind}"); + if (modelsTab) + { + if (_textureWorkspaceTabs[objectKind] != TextureWorkspaceTab.Models) + { + _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Models; + } + DrawModelWorkspace(modelGroup); + } + } + + if (hasOther) + { + using var otherFilesTab = ImRaii.TabItem($"Other file types###other_{objectKind}"); if (otherFilesTab) { if (_textureWorkspaceTabs[objectKind] != TextureWorkspaceTab.OtherFiles) @@ -1898,6 +1962,249 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } + private static void DrawPanelBox(string id, Vector4 background, Vector4 border, float rounding, Vector2 padding, Action content) + { + using (ImRaii.PushId(id)) + { + var startPos = ImGui.GetCursorScreenPos(); + var availableWidth = ImGui.GetContentRegionAvail().X; + var drawList = ImGui.GetWindowDrawList(); + + drawList.ChannelsSplit(2); + drawList.ChannelsSetCurrent(1); + + using (ImRaii.Group()) + { + ImGui.Dummy(new Vector2(0f, padding.Y)); + ImGui.Indent(padding.X); + content(); + ImGui.Unindent(padding.X); + ImGui.Dummy(new Vector2(0f, padding.Y)); + } + + var rectMin = startPos; + var rectMax = new Vector2(startPos.X + availableWidth, ImGui.GetItemRectMax().Y); + var borderThickness = MathF.Max(1f, ImGui.GetStyle().ChildBorderSize); + + drawList.ChannelsSetCurrent(0); + drawList.AddRectFilled(rectMin, rectMax, UiSharedService.Color(background), rounding); + drawList.AddRect(rectMin, rectMax, UiSharedService.Color(border), rounding, ImDrawFlags.None, borderThickness); + drawList.ChannelsMerge(); + } + } + + private void DrawModelWorkspace(IGrouping modelGroup) + { + var scale = ImGuiHelpers.GlobalScale; + ImGuiHelpers.ScaledDummy(0); + var accent = UIColors.Get("LightlessBlue"); + var baseItemSpacing = ImGui.GetStyle().ItemSpacing; + var warningAccent = UIColors.Get("LightlessOrange"); + var config = _playerPerformanceConfig.Current; + var showWarning = !_dismissedModelBatchWarning && config.ShowBatchModelDecimationWarning; + var sectionAvail = ImGui.GetContentRegionAvail().Y; + var childHeight = MathF.Max(0f, sectionAvail - 2f * scale); + var warningRectValid = false; + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(4f * scale, 2f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 4f * scale))) + using (var child = ImRaii.Child("modelFiles", new Vector2(-1f, childHeight), false)) + { + if (child) + { + warningRectValid = true; + using (ImRaii.Disabled(showWarning)) + { + var originalTotal = modelGroup.Sum(c => c.OriginalSize); + var compressedTotal = modelGroup.Sum(c => c.CompressedSize); + var triangleTotal = modelGroup.Sum(c => c.Triangles); + + var availableWidth = ImGui.GetContentRegionAvail().X; + var splitSpacingX = 4f * scale; + var spacingX = splitSpacingX; + var minDetailWidth = MinModelDetailPaneWidth * scale; + var maxDetailWidth = Math.Min(MaxModelDetailPaneWidth * scale, Math.Max(minDetailWidth, availableWidth - (360f * scale) - spacingX)); + var minTableWidth = 360f * scale; + + var detailWidth = Math.Clamp(_modelDetailPaneWidth, minDetailWidth, maxDetailWidth); + var tableWidth = availableWidth - detailWidth - spacingX; + if (tableWidth < minTableWidth) + { + detailWidth = Math.Max(0f, availableWidth - minTableWidth - spacingX); + tableWidth = availableWidth - detailWidth - spacingX; + if (tableWidth <= 0f) + { + tableWidth = availableWidth; + detailWidth = 0f; + } + } + if (detailWidth > 0f) + { + _modelDetailPaneWidth = detailWidth; + } + + ImGui.BeginGroup(); + using (var leftChild = ImRaii.Child("modelMainPane", new Vector2(detailWidth > 0f ? tableWidth : -1f, 0f), false)) + { + if (leftChild) + { + var badgeBg = new Vector4(accent.X, accent.Y, accent.Z, 0.18f); + var badgeBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f); + var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.6f, 36f * scale); + var summaryWidth = MathF.Min(520f * scale, ImGui.GetContentRegionAvail().X); + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 4f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(badgeBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(badgeBorder))) + using (var summaryChild = ImRaii.Child("modelSummary", new Vector2(summaryWidth, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (summaryChild) + { + var infoColor = ImGuiColors.DalamudGrey; + var countColor = UIColors.Get("LightlessBlue"); + var actualColor = ImGuiColors.DalamudGrey; + var compressedColor = UIColors.Get("LightlessYellow2"); + var triColor = UIColors.Get("LightlessPurple"); + + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(10f * scale, 4f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 2f * scale))) + using (var summaryTable = ImRaii.Table("modelSummaryTable", 4, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoHostExtendX, + new Vector2(-1f, -1f))) + { + if (summaryTable) + { + ImGui.TableNextRow(); + DrawSummaryCell(FontAwesomeIcon.LayerGroup, countColor, + modelGroup.Count().ToString("N0", CultureInfo.InvariantCulture), + "Model files", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.FileArchive, actualColor, + UiSharedService.ByteToString(originalTotal), + "Actual size", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, compressedColor, + UiSharedService.ByteToString(compressedTotal), + "Compressed size", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.ProjectDiagram, triColor, + triangleTotal.ToString("N0", CultureInfo.InvariantCulture), + "Triangles", infoColor, scale); + } + } + } + } + + if (_showModelBatchAdvancedSettings) + { + var splitterHeight = ModelBatchSplitterHeight * scale; + var minBatchHeight = 140f * scale; + var minTableHeight = 180f * scale; + var availableHeight = ImGui.GetContentRegionAvail().Y; + var decimationRunning = _modelDecimationTask != null && !_modelDecimationTask.IsCompleted; + var actionsHeight = ImGui.GetFrameHeightWithSpacing(); + if (decimationRunning) + { + actionsHeight += ImGui.GetFrameHeightWithSpacing(); + } + + var maxBatchHeight = Math.Max(minBatchHeight, availableHeight - minTableHeight - splitterHeight - actionsHeight); + if (_modelBatchPanelHeight <= 0f || _modelBatchPanelHeight > maxBatchHeight) + { + _modelBatchPanelHeight = Math.Min( + maxBatchHeight, + Math.Max(minBatchHeight, (availableHeight - actionsHeight) * 0.35f)); + } + + using (var batchChild = ImRaii.Child("modelBatchArea", new Vector2(-1f, _modelBatchPanelHeight), false)) + { + if (batchChild) + { + DrawModelBatchPanel(); + } + } + + DrawHorizontalResizeHandle("##modelBatchSplitter", ref _modelBatchPanelHeight, minBatchHeight, maxBatchHeight, out _); + + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, baseItemSpacing)) + { + DrawModelBatchActions(); + } + } + else + { + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, baseItemSpacing)) + { + DrawModelBatchPanel(); + DrawModelBatchActions(); + } + } + + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(4f * scale, 4f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(4f * scale, 3f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 3f * scale))) + { + DrawTable(modelGroup); + } + } + } + ImGui.EndGroup(); + + if (detailWidth > 0f) + { + var leftMin = ImGui.GetItemRectMin(); + var leftMax = ImGui.GetItemRectMax(); + var leftHeight = leftMax.Y - leftMin.Y; + var leftTopLocal = leftMin - ImGui.GetWindowPos(); + var maxDetailResize = Math.Min(MaxModelDetailPaneWidth * scale, Math.Max(minDetailWidth, availableWidth - minTableWidth - spacingX)); + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(splitSpacingX, ImGui.GetStyle().ItemSpacing.Y))) + { + DrawVerticalResizeHandle( + "##modelDetailSplitter", + leftTopLocal.Y, + leftHeight, + ref _modelDetailPaneWidth, + minDetailWidth, + maxDetailResize, + out _, + invert: true, + splitterWidthOverride: TextureDetailSplitterWidth); + } + + ImGui.BeginGroup(); + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(4f * scale, 4f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 4f * scale))) + using (var detailChild = ImRaii.Child("modelDetailPane", new Vector2(detailWidth, 0f), true)) + { + if (detailChild) + { + DrawModelDetailPane(modelGroup); + } + } + ImGui.EndGroup(); + } + } + } + } + + if (showWarning && warningRectValid) + { + if (!_modelBatchWarningPendingInitialized) + { + _modelBatchWarningNeverShowPending = !config.ShowBatchModelDecimationWarning; + _modelBatchWarningPendingInitialized = true; + } + + DrawModelBatchWarningOverlay(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), config, warningAccent); + } + else + { + _modelBatchWarningPendingInitialized = false; + } + } + private void DrawOtherFileWorkspace(IReadOnlyList> otherFileGroups) { if (otherFileGroups.Count == 0) @@ -2019,6 +2326,1008 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase DrawSelectedFileDetails(activeGroup); } + private void DrawModelBatchPanel() + { + var scale = ImGuiHelpers.GlobalScale; + var config = _playerPerformanceConfig.Current; + var accent = UIColors.Get("LightlessOrange"); + var panelBg = new Vector4(accent.X, accent.Y, accent.Z, 0.12f); + var panelBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f); + + DrawPanelBox("model-batch-panel", panelBg, panelBorder, 6f * scale, new Vector2(10f * scale, 6f * scale), () => + { + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 4f * scale))) + { + _uiSharedService.IconText(FontAwesomeIcon.ProjectDiagram, accent); + ImGui.SameLine(0f, 6f * scale); + UiSharedService.ColorText("Batch decimation", accent); + } + + UiSharedService.TextWrapped("Mark models in the table to add them to the decimation queue. Settings here apply only to batch decimation."); + + if (_modelDecimationFailed) + { + UiSharedService.ColorTextWrapped("Model decimation failed. Check logs for details.", UIColors.Get("DimRed")); + } + + ImGuiHelpers.ScaledDummy(4); + + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + using (var settingsTable = ImRaii.Table("modelBatchSettings", 2, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody)) + { + if (settingsTable) + { + ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); + ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Target triangle ratio"); + var defaultTargetPercent = (float)(ModelDecimationDefaults.BatchTargetRatio * 100.0); + UiSharedService.AttachToolTip($"Percentage of triangles to keep after decimation. Default: {defaultTargetPercent:0}%.\nRight-click to reset."); + + ImGui.TableSetColumnIndex(1); + var targetPercent = (float)(config.BatchModelDecimationTargetRatio * 100.0); + var clampedPercent = Math.Clamp(targetPercent, 1f, 99f); + if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon) + { + config.BatchModelDecimationTargetRatio = clampedPercent / 100.0; + _playerPerformanceConfig.Save(); + targetPercent = clampedPercent; + } + + ImGui.SetNextItemWidth(220f * scale); + if (ImGui.SliderFloat("##batch-decimation-target", ref targetPercent, 1f, 99f, "%.0f%%")) + { + config.BatchModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.01f, 0.99f); + _playerPerformanceConfig.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.BatchModelDecimationTargetRatio = ModelDecimationDefaults.BatchTargetRatio; + _playerPerformanceConfig.Save(); + } + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Normalize tangents"); + UiSharedService.AttachToolTip($"Normalize tangent vectors after decimation. Default: {(ModelDecimationDefaults.BatchNormalizeTangents ? "On" : "Off")}.\nRight-click to reset."); + + ImGui.TableSetColumnIndex(1); + var normalizeTangents = config.BatchModelDecimationNormalizeTangents; + if (UiSharedService.CheckboxWithBorder("##batch-decimation-normalize", ref normalizeTangents, accent, 1.5f)) + { + config.BatchModelDecimationNormalizeTangents = normalizeTangents; + _playerPerformanceConfig.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.BatchModelDecimationNormalizeTangents = ModelDecimationDefaults.BatchNormalizeTangents; + _playerPerformanceConfig.Save(); + } + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Avoid body intersection"); + UiSharedService.AttachToolTip($"Uses body materials as a collision guard to reduce clothing clipping. Slower and may reduce decimation. Default: {(ModelDecimationDefaults.BatchAvoidBodyIntersection ? "On" : "Off")}.\nRight-click to reset."); + + ImGui.TableSetColumnIndex(1); + var avoidBodyIntersection = config.BatchModelDecimationAvoidBodyIntersection; + if (UiSharedService.CheckboxWithBorder("##batch-decimation-body-collision", ref avoidBodyIntersection, accent, 1.5f)) + { + config.BatchModelDecimationAvoidBodyIntersection = avoidBodyIntersection; + _playerPerformanceConfig.Save(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + config.BatchModelDecimationAvoidBodyIntersection = ModelDecimationDefaults.BatchAvoidBodyIntersection; + _playerPerformanceConfig.Save(); + } + } + } + + ImGuiHelpers.ScaledDummy(4); + var showAdvanced = _showModelBatchAdvancedSettings; + if (UiSharedService.CheckboxWithBorder("##batch-decimation-advanced-toggle", ref showAdvanced, accent, 1.5f)) + { + _showModelBatchAdvancedSettings = showAdvanced; + } + + ImGui.SameLine(0f, 6f * scale); + ImGui.TextUnformatted("Advanced settings"); + ImGuiHelpers.ScaledDummy(2); + UiSharedService.ColorTextWrapped("Applies to automatic and batch decimation.", UIColors.Get("LightlessGrey")); + + if (_showModelBatchAdvancedSettings) + { + ImGuiHelpers.ScaledDummy(4); + DrawModelBatchAdvancedSettings(config, accent); + } + + ImGuiHelpers.ScaledDummy(4); + }); + } + + private void DrawModelBatchWarningOverlay(Vector2 panelMin, Vector2 panelMax, PlayerPerformanceConfig config, Vector4 accent) + { + var scale = ImGuiHelpers.GlobalScale; + var overlaySize = panelMax - panelMin; + + if (overlaySize.X <= 0f || overlaySize.Y <= 0f) + { + return; + } + + var previousCursor = ImGui.GetCursorPos(); + var windowPos = ImGui.GetWindowPos(); + ImGui.SetCursorPos(panelMin - windowPos); + + var bgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg]; + bgColor.W = 0.9f; + + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 6f * scale); + ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 0f); + ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ChildBg, bgColor); + + var overlayFlags = ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.NoScrollWithMouse + | ImGuiWindowFlags.NoSavedSettings; + + if (ImGui.BeginChild("##model_decimation_warning_overlay", overlaySize, false, overlayFlags)) + { + var contentMin = ImGui.GetWindowContentRegionMin(); + var contentMax = ImGui.GetWindowContentRegionMax(); + var contentSize = contentMax - contentMin; + var text = "Model decimation is a destructive process but the algorithm was built with multiple safety features to avoid damage to the mesh and prevent clipping.\nIt is advised to back up your important mods or models/meshes before running decimation as it's not recoverable."; + var cardWidth = MathF.Min(520f * scale, contentSize.X - (32f * scale)); + cardWidth = MathF.Max(cardWidth, 320f * scale); + var cardPadding = new Vector2(12f * scale, 10f * scale); + var wrapWidth = cardWidth - (cardPadding.X * 2f); + var textSize = ImGui.CalcTextSize(text, false, wrapWidth); + var headerHeight = ImGui.GetTextLineHeightWithSpacing(); + var rowHeight = MathF.Max(ImGui.GetFrameHeight(), headerHeight); + var buttonHeight = ImGui.GetFrameHeight(); + var mediumGap = 6f * scale; + var headerGap = 4f * scale; + var cardHeight = (cardPadding.Y * 2f) + + headerHeight + + headerGap + + textSize.Y + + mediumGap + + rowHeight + + mediumGap + + buttonHeight; + + var cardMin = new Vector2( + contentMin.X + Math.Max(0f, (contentSize.X - cardWidth) * 0.5f), + contentMin.Y + Math.Max(0f, (contentSize.Y - cardHeight) * 0.5f)); + var cardMax = cardMin + new Vector2(cardWidth, cardHeight); + var cardMinScreen = ImGui.GetWindowPos() + cardMin; + var cardMaxScreen = ImGui.GetWindowPos() + cardMax; + + var drawList = ImGui.GetWindowDrawList(); + var cardBg = new Vector4(accent.X, accent.Y, accent.Z, 0.24f); + var cardBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.6f); + drawList.AddRectFilled(cardMinScreen, cardMaxScreen, UiSharedService.Color(cardBg), 6f * scale); + drawList.AddRect(cardMinScreen, cardMaxScreen, UiSharedService.Color(cardBorder), 6f * scale); + + var baseX = cardMin.X + cardPadding.X; + var currentY = cardMin.Y + cardPadding.Y; + + ImGui.SetCursorPos(new Vector2(baseX, currentY)); + var warningColor = UIColors.Get("LightlessYellow"); + _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, warningColor); + ImGui.SameLine(0f, 6f * scale); + UiSharedService.ColorText("Model Decimation", warningColor); + + currentY += headerHeight + headerGap; + ImGui.SetCursorPos(new Vector2(baseX, currentY)); + ImGui.PushTextWrapPos(baseX + wrapWidth); + ImGui.TextUnformatted(text); + ImGui.PopTextWrapPos(); + + currentY += textSize.Y + mediumGap; + ImGui.SetCursorPos(new Vector2(baseX, currentY)); + + var neverShowAgain = _modelBatchWarningNeverShowPending; + if (UiSharedService.CheckboxWithBorder("##batch-decimation-warning-never", ref neverShowAgain, accent, 1.5f)) + { + _modelBatchWarningNeverShowPending = neverShowAgain; + } + ImGui.SameLine(0f, 6f * scale); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Never show again"); + + currentY += rowHeight + mediumGap; + var buttonWidth = 200f * scale; + var buttonX = cardMin.X + Math.Max(0f, (cardWidth - buttonWidth) * 0.5f); + ImGui.SetCursorPos(new Vector2(buttonX, currentY)); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "I understand", buttonWidth, center: true)) + { + config.ShowBatchModelDecimationWarning = !_modelBatchWarningNeverShowPending; + _playerPerformanceConfig.Save(); + _dismissedModelBatchWarning = true; + } + } + + ImGui.EndChild(); + ImGui.PopStyleColor(2); + ImGui.PopStyleVar(2); + ImGui.SetCursorPos(previousCursor); + } + + private void DrawModelBatchAdvancedSettings(PlayerPerformanceConfig config, Vector4 accent) + { + var scale = ImGuiHelpers.GlobalScale; + var advanced = config.ModelDecimationAdvanced; + var labelWidth = 190f * scale; + var itemWidth = -1f; + + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + using (var table = ImRaii.Table("modelBatchAdvancedSettings", 4, ImGuiTableFlags.SizingStretchSame | ImGuiTableFlags.NoBordersInBody)) + { + if (!table) + { + return; + } + + ImGui.TableSetupColumn("LabelLeft", ImGuiTableColumnFlags.WidthFixed, labelWidth); + ImGui.TableSetupColumn("ControlLeft", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("LabelRight", ImGuiTableColumnFlags.WidthFixed, labelWidth); + ImGui.TableSetupColumn("ControlRight", ImGuiTableColumnFlags.WidthStretch); + + var triangleThreshold = config.BatchModelDecimationTriangleThreshold; + DrawBatchAdvancedCategoryRow( + "Component limits", + "Limits that decide which meshes or components are eligible for batch decimation.", + scale); + ImGui.TableNextRow(); + if (DrawBatchAdvancedIntCell( + "Triangle threshold", + "batch-adv-triangle-threshold", + triangleThreshold, + 0, + 100_000, + ModelDecimationDefaults.BatchTriangleThreshold, + "Skip meshes below this triangle count during batch decimation (0 disables).", + itemWidth, + out var triangleThresholdValue)) + { + config.BatchModelDecimationTriangleThreshold = triangleThresholdValue; + _playerPerformanceConfig.Save(); + } + + var minComponentTriangles = advanced.MinComponentTriangles; + if (DrawBatchAdvancedIntCell( + "Min component triangles", + "batch-adv-min-component", + minComponentTriangles, + 0, + 200, + ModelDecimationAdvancedSettings.DefaultMinComponentTriangles, + "Components at or below this triangle count are left untouched.", + itemWidth, + out var minComponentTrianglesValue)) + { + advanced.MinComponentTriangles = minComponentTrianglesValue; + _playerPerformanceConfig.Save(); + } + + var maxEdgeFactor = advanced.MaxCollapseEdgeLengthFactor; + DrawBatchAdvancedCategoryRow( + "Collapse guards", + "Quality and topology guards that block unsafe edge collapses.", + scale); + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Max edge length factor", + "batch-adv-max-edge", + maxEdgeFactor, + 0.1f, + 5f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultMaxCollapseEdgeLengthFactor, + "Caps collapses to (average edge length * factor).", + itemWidth, + out var maxEdgeFactorValue)) + { + advanced.MaxCollapseEdgeLengthFactor = maxEdgeFactorValue; + _playerPerformanceConfig.Save(); + } + + var normalSimilarity = advanced.NormalSimilarityThresholdDegrees; + if (DrawBatchAdvancedFloatCell( + "Normal similarity (deg)", + "batch-adv-normal-sim", + normalSimilarity, + 0f, + 180f, + 1f, + "%.0f", + ModelDecimationAdvancedSettings.DefaultNormalSimilarityThresholdDegrees, + "Block collapses that bend normals beyond this angle.", + itemWidth, + out var normalSimilarityValue)) + { + advanced.NormalSimilarityThresholdDegrees = normalSimilarityValue; + _playerPerformanceConfig.Save(); + } + + var boneWeightSimilarity = advanced.BoneWeightSimilarityThreshold; + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Bone weight similarity", + "batch-adv-bone-sim", + boneWeightSimilarity, + 0f, + 1f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBoneWeightSimilarityThreshold, + "Requires this bone-weight overlap to allow a collapse.", + itemWidth, + out var boneWeightSimilarityValue)) + { + advanced.BoneWeightSimilarityThreshold = boneWeightSimilarityValue; + _playerPerformanceConfig.Save(); + } + + var uvSimilarity = advanced.UvSimilarityThreshold; + if (DrawBatchAdvancedFloatCell( + "UV similarity threshold", + "batch-adv-uv-sim", + uvSimilarity, + 0f, + 0.5f, + 0.005f, + "%.3f", + ModelDecimationAdvancedSettings.DefaultUvSimilarityThreshold, + "Blocks collapses when UVs diverge beyond this threshold.", + itemWidth, + out var uvSimilarityValue)) + { + advanced.UvSimilarityThreshold = uvSimilarityValue; + _playerPerformanceConfig.Save(); + } + + var uvSeamCos = advanced.UvSeamAngleCos; + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "UV seam cosine", + "batch-adv-uv-seam-cos", + uvSeamCos, + -1f, + 1f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultUvSeamAngleCos, + "Cosine threshold for UV seam detection (higher is stricter).", + itemWidth, + out var uvSeamCosValue)) + { + advanced.UvSeamAngleCos = uvSeamCosValue; + _playerPerformanceConfig.Save(); + } + + var blockUvSeams = advanced.BlockUvSeamVertices; + if (DrawBatchAdvancedBoolCell( + "Block UV seam vertices", + "batch-adv-uv-block", + blockUvSeams, + ModelDecimationAdvancedSettings.DefaultBlockUvSeamVertices, + "Prevent collapses across UV seams.", + accent, + out var blockUvSeamsValue)) + { + advanced.BlockUvSeamVertices = blockUvSeamsValue; + _playerPerformanceConfig.Save(); + } + + var allowBoundary = advanced.AllowBoundaryCollapses; + ImGui.TableNextRow(); + if (DrawBatchAdvancedBoolCell( + "Allow boundary collapses", + "batch-adv-boundary", + allowBoundary, + ModelDecimationAdvancedSettings.DefaultAllowBoundaryCollapses, + "Allow collapses on mesh boundaries (can create holes).", + accent, + out var allowBoundaryValue)) + { + advanced.AllowBoundaryCollapses = allowBoundaryValue; + _playerPerformanceConfig.Save(); + } + + var bodyDistance = advanced.BodyCollisionDistanceFactor; + DrawBatchAdvancedEmptyCell(); + + var bodyNoOpDistance = advanced.BodyCollisionNoOpDistanceFactor; + DrawBatchAdvancedCategoryRow( + "Body collision", + "Controls how the body mesh is used as a collision guard to reduce clothing clipping.", + scale); + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Body collision distance", + "batch-adv-body-distance", + bodyDistance, + 0f, + 5f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionDistanceFactor, + "Primary body collision distance factor.", + itemWidth, + out var bodyDistanceValue)) + { + advanced.BodyCollisionDistanceFactor = bodyDistanceValue; + _playerPerformanceConfig.Save(); + } + + if (DrawBatchAdvancedFloatCell( + "Body collision fallback distance", + "batch-adv-body-noop", + bodyNoOpDistance, + 0f, + 5f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpDistanceFactor, + "Fallback body collision distance for relaxed pass.", + itemWidth, + out var bodyNoOpDistanceValue)) + { + advanced.BodyCollisionNoOpDistanceFactor = bodyNoOpDistanceValue; + _playerPerformanceConfig.Save(); + } + + var bodyRelax = advanced.BodyCollisionAdaptiveRelaxFactor; + var bodyNearRatio = advanced.BodyCollisionAdaptiveNearRatio; + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Body collision relax factor", + "batch-adv-body-relax", + bodyRelax, + 0f, + 5f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveRelaxFactor, + "Multiplier applied when the mesh is near the body.", + itemWidth, + out var bodyRelaxValue)) + { + advanced.BodyCollisionAdaptiveRelaxFactor = bodyRelaxValue; + _playerPerformanceConfig.Save(); + } + + if (DrawBatchAdvancedFloatCell( + "Body collision near ratio", + "batch-adv-body-near", + bodyNearRatio, + 0f, + 1f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveNearRatio, + "Fraction of vertices near the body required to relax.", + itemWidth, + out var bodyNearRatioValue)) + { + advanced.BodyCollisionAdaptiveNearRatio = bodyNearRatioValue; + _playerPerformanceConfig.Save(); + } + + var bodyUvRelax = advanced.BodyCollisionAdaptiveUvThreshold; + var bodyNoOpUvCos = advanced.BodyCollisionNoOpUvSeamAngleCos; + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Body collision UV relax", + "batch-adv-body-uv", + bodyUvRelax, + 0f, + 0.5f, + 0.005f, + "%.3f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveUvThreshold, + "UV similarity threshold used in relaxed mode.", + itemWidth, + out var bodyUvRelaxValue)) + { + advanced.BodyCollisionAdaptiveUvThreshold = bodyUvRelaxValue; + _playerPerformanceConfig.Save(); + } + + if (DrawBatchAdvancedFloatCell( + "Body collision UV cosine", + "batch-adv-body-uv-cos", + bodyNoOpUvCos, + -1f, + 1f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpUvSeamAngleCos, + "UV seam cosine used in relaxed mode.", + itemWidth, + out var bodyNoOpUvCosValue)) + { + advanced.BodyCollisionNoOpUvSeamAngleCos = bodyNoOpUvCosValue; + _playerPerformanceConfig.Save(); + } + + var bodyProtection = advanced.BodyCollisionProtectionFactor; + var bodyProxyMin = advanced.BodyProxyTargetRatioMin; + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Body collision protection", + "batch-adv-body-protect", + bodyProtection, + 0f, + 5f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionProtectionFactor, + "Expansion factor for protected vertices near the body.", + itemWidth, + out var bodyProtectionValue)) + { + advanced.BodyCollisionProtectionFactor = bodyProtectionValue; + _playerPerformanceConfig.Save(); + } + + if (DrawBatchAdvancedFloatCell( + "Body proxy min ratio", + "batch-adv-body-proxy", + bodyProxyMin, + 0f, + 1f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyProxyTargetRatioMin, + "Minimum target ratio when decimating the body proxy.", + itemWidth, + out var bodyProxyMinValue)) + { + advanced.BodyProxyTargetRatioMin = bodyProxyMinValue; + _playerPerformanceConfig.Save(); + } + + var bodyInflate = advanced.BodyCollisionProxyInflate; + var bodyPenetration = advanced.BodyCollisionPenetrationFactor; + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Body collision inflate", + "batch-adv-body-inflate", + bodyInflate, + 0f, + 0.01f, + 0.0001f, + "%.4f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionProxyInflate, + "Inflate body collision distances by this offset.", + itemWidth, + out var bodyInflateValue)) + { + advanced.BodyCollisionProxyInflate = bodyInflateValue; + _playerPerformanceConfig.Save(); + } + + if (DrawBatchAdvancedFloatCell( + "Body penetration factor", + "batch-adv-body-penetration", + bodyPenetration, + 0f, + 1f, + 0.01f, + "%.2f", + ModelDecimationAdvancedSettings.DefaultBodyCollisionPenetrationFactor, + "Reject collapses that penetrate the body below this factor.", + itemWidth, + out var bodyPenetrationValue)) + { + advanced.BodyCollisionPenetrationFactor = bodyPenetrationValue; + _playerPerformanceConfig.Save(); + } + + var minBodyDistance = advanced.MinBodyCollisionDistance; + var minBodyCell = advanced.MinBodyCollisionCellSize; + ImGui.TableNextRow(); + if (DrawBatchAdvancedFloatCell( + "Min body collision distance", + "batch-adv-body-min-dist", + minBodyDistance, + 1e-6f, + 0.01f, + 0.00001f, + "%.6f", + ModelDecimationAdvancedSettings.DefaultMinBodyCollisionDistance, + "Lower bound for body collision distance.", + itemWidth, + out var minBodyDistanceValue)) + { + advanced.MinBodyCollisionDistance = minBodyDistanceValue; + _playerPerformanceConfig.Save(); + } + + if (DrawBatchAdvancedFloatCell( + "Min body collision cell size", + "batch-adv-body-min-cell", + minBodyCell, + 1e-6f, + 0.01f, + 0.00001f, + "%.6f", + ModelDecimationAdvancedSettings.DefaultMinBodyCollisionCellSize, + "Lower bound for the body collision grid size.", + itemWidth, + out var minBodyCellValue)) + { + advanced.MinBodyCollisionCellSize = minBodyCellValue; + _playerPerformanceConfig.Save(); + } + } + } + + private bool DrawBatchAdvancedIntCell( + string label, + string id, + int currentValue, + int minValue, + int maxValue, + int defaultValue, + string tooltip, + float itemWidth, + out int newValue) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + UiSharedService.AttachToolTip($"{tooltip}\nDefault: {defaultValue:N0}. Right-click to reset."); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(itemWidth); + newValue = currentValue; + var changed = ImGui.DragInt($"##{id}", ref newValue, 1f, minValue, maxValue); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + newValue = defaultValue; + changed = true; + } + + return changed; + } + + private bool DrawBatchAdvancedFloatCell( + string label, + string id, + float currentValue, + float minValue, + float maxValue, + float speed, + string format, + float defaultValue, + string tooltip, + float itemWidth, + out float newValue) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + var defaultText = defaultValue.ToString("0.#######", CultureInfo.InvariantCulture); + UiSharedService.AttachToolTip($"{tooltip}\nDefault: {defaultText}. Right-click to reset."); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(itemWidth); + newValue = currentValue; + var changed = ImGui.DragFloat($"##{id}", ref newValue, speed, minValue, maxValue, format); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + newValue = defaultValue; + changed = true; + } + + return changed; + } + + private bool DrawBatchAdvancedBoolCell( + string label, + string id, + bool currentValue, + bool defaultValue, + string tooltip, + Vector4 accent, + out bool newValue) + { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + UiSharedService.AttachToolTip($"{tooltip}\nDefault: {(defaultValue ? "On" : "Off")}. Right-click to reset."); + + ImGui.TableNextColumn(); + newValue = currentValue; + var changed = UiSharedService.CheckboxWithBorder($"##{id}", ref newValue, accent, 1.5f); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + newValue = defaultValue; + changed = true; + } + + return changed; + } + + private static void DrawBatchAdvancedEmptyCell() + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + + private void DrawBatchAdvancedCategoryRow(string label, string tooltip, float scale) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(UIColors.Get("LightlessBlue"), label); + _uiSharedService.DrawHelpText(tooltip); + ImGui.TableSetColumnIndex(1); + ImGui.Dummy(Vector2.Zero); + ImGui.TableSetColumnIndex(2); + ImGui.Dummy(Vector2.Zero); + ImGui.TableSetColumnIndex(3); + ImGui.Dummy(Vector2.Zero); + } + + private void DrawModelBatchActions() + { + var scale = ImGuiHelpers.GlobalScale; + PruneModelSelections(); + var selectionCount = _selectedModelKeys.Count; + var decimationRunning = _modelDecimationTask != null && !_modelDecimationTask.IsCompleted; + + using (ImRaii.Disabled(decimationRunning || selectionCount == 0)) + { + var label = selectionCount > 0 ? $"Decimate {selectionCount} selected" : "Decimate selected"; + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ProjectDiagram, label, 220f * scale)) + { + StartModelDecimationBatch(); + } + } + + ImGui.SameLine(); + using (ImRaii.Disabled(decimationRunning || _selectedModelKeys.Count == 0)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear marks", 160f * scale)) + { + _selectedModelKeys.Clear(); + } + } + + if (decimationRunning) + { + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel", 120f * scale)) + { + _modelDecimationCts.Cancel(); + } + } + + ImGui.SameLine(); + var searchWidth = 220f * scale; + var searchStartX = ImGui.GetCursorPosX(); + var searchAvail = ImGui.GetContentRegionAvail().X; + var searchX = searchStartX + Math.Max(0f, searchAvail - searchWidth); + ImGui.SetCursorPosX(searchX); + ImGui.SetNextItemWidth(searchWidth); + var search = _modelSearch; + if (ImGui.InputTextWithHint("##model-search", "Search models...", ref search, 128)) + { + _modelSearch = search; + } + UiSharedService.AttachToolTip("Filter model rows by name, hash, or path."); + + if (decimationRunning) + { + var total = Math.Max(_modelDecimationTotalJobs, 1); + var completed = Math.Clamp(_modelDecimationCurrentProgress, 0, total); + var progress = (float)completed / total; + var label = string.IsNullOrEmpty(_modelDecimationCurrentHash) + ? $"{completed}/{total}" + : $"{completed}/{total} • {_modelDecimationCurrentHash}"; + ImGui.ProgressBar(progress, new Vector2(-1f, 0f), label); + } + } + + private void DrawModelDetailPane(IGrouping modelGroup) + { + var scale = ImGuiHelpers.GlobalScale; + CharacterAnalyzer.FileDataEntry? selected = null; + if (!string.IsNullOrEmpty(_selectedHash)) + { + selected = modelGroup.FirstOrDefault(entry => string.Equals(entry.Hash, _selectedHash, StringComparison.Ordinal)); + } + + UiSharedService.ColorText("Model Details", UIColors.Get("LightlessPurple")); + if (selected != null) + { + var sourcePath = selected.FilePaths.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(sourcePath)) + { + ImGui.SameLine(); + ImGui.TextUnformatted(Path.GetFileName(sourcePath)); + UiSharedService.AttachToolTip("Source file: " + sourcePath); + } + } + ImGui.Separator(); + + if (selected == null) + { + UiSharedService.ColorText("Select a model to view details.", ImGuiColors.DalamudGrey); + return; + } + + using (ImRaii.Child("modelDetailInfo", new Vector2(-1f, 0f), true, ImGuiWindowFlags.AlwaysVerticalScrollbar)) + { + var labelColor = ImGuiColors.DalamudGrey; + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + { + var metaFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX; + if (ImGui.BeginTable("modelMetaOverview", 2, metaFlags)) + { + MetaRow(FontAwesomeIcon.Cube, "Object", _selectedObjectTab.ToString()); + MetaRow(FontAwesomeIcon.Fingerprint, "Hash", selected.Hash, UIColors.Get("LightlessBlue")); + + var pendingColor = selected.IsComputed ? (Vector4?)null : UIColors.Get("LightlessYellow"); + var triangleLabel = selected.IsComputed + ? selected.Triangles.ToString("N0", CultureInfo.InvariantCulture) + : "Pending"; + MetaRow(FontAwesomeIcon.ProjectDiagram, "Triangles", triangleLabel, pendingColor); + + var originalLabel = selected.IsComputed + ? UiSharedService.ByteToString(selected.OriginalSize) + : "Pending"; + var compressedLabel = selected.IsComputed + ? UiSharedService.ByteToString(selected.CompressedSize) + : "Pending"; + MetaRow(FontAwesomeIcon.Database, "Original", originalLabel, pendingColor); + MetaRow(FontAwesomeIcon.CompressArrowsAlt, "Compressed", compressedLabel, pendingColor); + + ImGui.EndTable(); + } + } + + ImGuiHelpers.ScaledDummy(4); + + if (selected.IsComputed) + { + var savedBytes = selected.OriginalSize - selected.CompressedSize; + var savedMagnitude = Math.Abs(savedBytes); + var savedColor = savedBytes > 0 ? UIColors.Get("LightlessGreen") + : savedBytes < 0 ? UIColors.Get("DimRed") + : ImGuiColors.DalamudGrey; + var savedLabel = savedBytes > 0 ? "Saved" : savedBytes < 0 ? "Over" : "Delta"; + var savedPercent = selected.OriginalSize > 0 && savedMagnitude > 0 + ? $"{savedMagnitude * 100d / selected.OriginalSize:0.#}%" + : null; + + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + { + var statFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX; + if (ImGui.BeginTable("modelSizeSummary", 3, statFlags)) + { + ImGui.TableNextRow(); + StatCell(FontAwesomeIcon.Database, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(selected.OriginalSize), "Original"); + StatCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(selected.CompressedSize), "Compressed"); + StatCell(FontAwesomeIcon.ChartLine, savedColor, savedMagnitude > 0 ? UiSharedService.ByteToString(savedMagnitude) : "No change", savedLabel, savedPercent, savedColor); + ImGui.EndTable(); + } + } + } + else + { + UiSharedService.ColorTextWrapped("Size and triangle data are still being computed.", UIColors.Get("LightlessYellow")); + } + + ImGuiHelpers.ScaledDummy(6); + DrawPathList("File Paths", selected.FilePaths, "No file paths recorded."); + DrawPathList("Game Paths", selected.GamePaths, "No game paths recorded."); + + void MetaRow(FontAwesomeIcon icon, string label, string value, Vector4? valueColor = null) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + _uiSharedService.IconText(icon, labelColor); + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, labelColor)) + { + ImGui.TextUnformatted(label); + } + + ImGui.TableNextColumn(); + if (valueColor.HasValue) + { + using (ImRaii.PushColor(ImGuiCol.Text, valueColor.Value)) + { + ImGui.TextUnformatted(value); + } + } + else + { + ImGui.TextUnformatted(value); + } + } + + void StatCell(FontAwesomeIcon icon, Vector4 iconColor, string mainText, string caption, string? extra = null, Vector4? extraColor = null) + { + ImGui.TableNextColumn(); + _uiSharedService.IconText(icon, iconColor); + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, iconColor)) + { + ImGui.TextUnformatted(mainText); + } + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + ImGui.TextUnformatted(caption); + } + if (!string.IsNullOrEmpty(extra)) + { + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, extraColor ?? iconColor)) + { + ImGui.TextUnformatted(extra); + } + } + } + + void DrawPathList(string title, IReadOnlyList entries, string emptyMessage) + { + var count = entries.Count; + using var headerDefault = ImRaii.PushColor(ImGuiCol.Header, UiSharedService.Color(new Vector4(0.15f, 0.15f, 0.18f, 0.95f))); + using var headerHover = ImRaii.PushColor(ImGuiCol.HeaderHovered, UiSharedService.Color(new Vector4(0.2f, 0.2f, 0.25f, 1f))); + using var headerActive = ImRaii.PushColor(ImGuiCol.HeaderActive, UiSharedService.Color(new Vector4(0.25f, 0.25f, 0.3f, 1f))); + var label = $"{title} ({count})"; + if (!ImGui.CollapsingHeader(label, count == 0 ? ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.None)) + { + return; + } + + if (count == 0) + { + UiSharedService.ColorText(emptyMessage, ImGuiColors.DalamudGrey); + return; + } + + var tableFlags = ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.BordersOuter; + if (ImGui.BeginTable($"{title}Table", 2, tableFlags)) + { + ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 28f * scale); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + for (int i = 0; i < entries.Count; i++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{i + 1}."); + + ImGui.TableNextColumn(); + var wrapPos = ImGui.GetCursorPosX() + ImGui.GetColumnWidth(); + ImGui.PushTextWrapPos(wrapPos); + ImGui.TextUnformatted(entries[i]); + ImGui.PopTextWrapPos(); + } + + ImGui.EndTable(); + } + } + } + } + private void DrawSelectedFileDetails(IGrouping? fileGroup) { var hasGroup = fileGroup != null; @@ -2411,6 +3720,39 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _showModal = true; } + private void StartModelDecimationBatch() + { + if (_modelDecimationTask != null && !_modelDecimationTask.IsCompleted) + { + return; + } + + if (_cachedAnalysis == null) + { + return; + } + + var selectedEntries = _cachedAnalysis.Values + .SelectMany(entries => entries.Values) + .Where(entry => string.Equals(entry.FileType, "mdl", StringComparison.OrdinalIgnoreCase)) + .Where(entry => _selectedModelKeys.Contains(entry.Hash)) + .ToList(); + + if (selectedEntries.Count == 0) + { + return; + } + + _modelDecimationCts = _modelDecimationCts.CancelRecreate(); + _modelDecimationTotalJobs = selectedEntries.Count; + _modelDecimationCurrentProgress = 0; + _modelDecimationCurrentHash = string.Empty; + _modelDecimationFailed = false; + + var settings = GetBatchDecimationSettings(); + _modelDecimationTask = RunModelDecimationAsync(selectedEntries, settings, _modelDecimationCts.Token); + } + private async Task RunTextureConversionAsync(List requests, CancellationToken token) { try @@ -2432,7 +3774,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { try { - await _characterAnalyzer.UpdateFileEntriesAsync(affectedPaths, token).ConfigureAwait(false); + await _characterAnalyzer.UpdateFileEntriesAsync(affectedPaths, token, force: true).ConfigureAwait(false); _hasUpdate = true; } catch (OperationCanceledException) @@ -2463,6 +3805,81 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } + private async Task RunModelDecimationAsync( + List entries, + ModelDecimationSettings settings, + CancellationToken token) + { + var affectedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + try + { + var completed = 0; + foreach (var entry in entries) + { + token.ThrowIfCancellationRequested(); + + var sourcePath = entry.FilePaths.FirstOrDefault(); + _modelDecimationCurrentHash = string.IsNullOrWhiteSpace(sourcePath) + ? entry.Hash + : Path.GetFileName(sourcePath); + _modelDecimationCurrentProgress = completed; + if (string.IsNullOrWhiteSpace(sourcePath)) + { + completed++; + continue; + } + + _modelDecimationService.ScheduleBatchDecimation(entry.Hash, sourcePath, settings); + await _modelDecimationService.WaitForPendingJobsAsync(new[] { entry.Hash }, token).ConfigureAwait(false); + + affectedPaths.Add(sourcePath); + completed++; + _modelDecimationCurrentProgress = completed; + } + + if (!token.IsCancellationRequested && affectedPaths.Count > 0) + { + await _characterAnalyzer.UpdateFileEntriesAsync(affectedPaths, token, force: true).ConfigureAwait(false); + _hasUpdate = true; + try + { + _ipcManager.Penumbra.RequestImmediateRedraw(0, RedrawType.Redraw); + } + catch (Exception redrawEx) + { + _logger.LogWarning(redrawEx, "Failed to request redraw after batch model decimation."); + } + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("Model decimation batch was cancelled."); + } + catch (Exception ex) + { + _modelDecimationFailed = true; + _logger.LogError(ex, "Model decimation batch failed."); + } + finally + { + _modelDecimationCurrentHash = string.Empty; + _selectedModelKeys.Clear(); + } + } + + private ModelDecimationSettings GetBatchDecimationSettings() + { + var config = _playerPerformanceConfig.Current; + var ratio = Math.Clamp(config.BatchModelDecimationTargetRatio, 0.01, 0.99); + var advanced = config.ModelDecimationAdvanced; + return new ModelDecimationSettings( + Math.Max(0, config.BatchModelDecimationTriangleThreshold), + ratio, + config.BatchModelDecimationNormalizeTangents, + config.BatchModelDecimationAvoidBodyIntersection, + advanced); + } + private bool DrawVerticalResizeHandle( string id, float topY, @@ -2473,12 +3890,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase out bool isDragging, bool invert = false, bool showToggle = false, - bool isCollapsed = false) + bool isCollapsed = false, + float? splitterWidthOverride = null) { var scale = ImGuiHelpers.GlobalScale; - var splitterWidth = (showToggle + var baseWidth = splitterWidthOverride ?? (showToggle ? (isCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) - : TextureFilterSplitterWidth) * scale; + : TextureFilterSplitterWidth); + var splitterWidth = baseWidth * scale; ImGui.SameLine(); var cursor = ImGui.GetCursorPos(); var contentMin = ImGui.GetWindowContentRegionMin(); @@ -2577,6 +3996,55 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase return toggleClicked; } + private void DrawHorizontalResizeHandle( + string id, + ref float topHeight, + float minHeight, + float maxHeight, + out bool isDragging, + bool invert = false, + float? splitterHeightOverride = null) + { + var scale = ImGuiHelpers.GlobalScale; + var baseHeight = splitterHeightOverride ?? ModelBatchSplitterHeight; + var splitterHeight = baseHeight * scale; + var width = ImGui.GetContentRegionAvail().X; + if (width <= 0f || splitterHeight <= 0f) + { + isDragging = false; + return; + } + + ImGui.InvisibleButton(id, new Vector2(width, splitterHeight)); + var drawList = ImGui.GetWindowDrawList(); + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + var windowPos = ImGui.GetWindowPos(); + var contentMin = ImGui.GetWindowContentRegionMin(); + var contentMax = ImGui.GetWindowContentRegionMax(); + var clipMin = windowPos + contentMin; + var clipMax = windowPos + contentMax; + drawList.PushClipRect(clipMin, clipMax, true); + + var hovered = ImGui.IsItemHovered(); + isDragging = ImGui.IsItemActive(); + var baseColor = UIColors.Get("ButtonDefault"); + var hoverColor = UIColors.Get("LightlessPurple"); + var activeColor = UIColors.Get("LightlessPurpleActive"); + var handleColor = isDragging ? activeColor : hovered ? hoverColor : baseColor; + var rounding = ImGui.GetStyle().FrameRounding; + drawList.AddRectFilled(rectMin, rectMax, UiSharedService.Color(handleColor), rounding); + drawList.AddRect(rectMin, rectMax, UiSharedService.Color(new Vector4(1f, 1f, 1f, 0.12f)), rounding); + drawList.PopClipRect(); + + if (isDragging) + { + var delta = ImGui.GetIO().MouseDelta.Y / scale; + topHeight += invert ? -delta : delta; + topHeight = Math.Clamp(topHeight, minHeight, maxHeight); + } + } + private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row) { var key = row.Key; @@ -3370,24 +4838,33 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private void DrawTable(IGrouping fileGroup) { - var tableColumns = string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 6 : 5; + var isModel = string.Equals(fileGroup.Key, "mdl", StringComparison.OrdinalIgnoreCase); + var tableColumns = 5; var scale = ImGuiHelpers.GlobalScale; + var selectionAccent = UIColors.Get("LightlessOrange"); using var table = ImRaii.Table("Analysis", tableColumns, - ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersInnerV, + ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoHostExtendX, new Vector2(-1f, 0f)); if (!table.Success) { return; } - ImGui.TableSetupColumn("Hash"); - ImGui.TableSetupColumn("Filepaths"); - ImGui.TableSetupColumn("Gamepaths"); - ImGui.TableSetupColumn("Original Size"); - ImGui.TableSetupColumn("Compressed Size"); - if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) + if (isModel) { - ImGui.TableSetupColumn("Triangles"); + ImGui.TableSetupColumn("##select", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, 32f * scale); + ImGui.TableSetupColumn("Model", ImGuiTableColumnFlags.WidthFixed, 380f * scale); + ImGui.TableSetupColumn("Triangles", ImGuiTableColumnFlags.WidthFixed, 120f * scale); + ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.WidthFixed, 140f * scale); + ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.WidthFixed, 140f * scale); + } + else + { + ImGui.TableSetupColumn("Hash", ImGuiTableColumnFlags.WidthFixed, 320f * scale); + ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.WidthFixed, 140f * scale); + ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.WidthFixed, 140f * scale); + ImGui.TableSetupColumn("File paths", ImGuiTableColumnFlags.WidthFixed, 90f * scale); + ImGui.TableSetupColumn("Game paths", ImGuiTableColumnFlags.WidthFixed, 90f * scale); } ImGui.TableSetupScrollFreeze(0, 1); @@ -3399,73 +4876,192 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase var spec = sortSpecs.Specs[0]; bool ascending = spec.SortDirection == ImGuiSortDirection.Ascending; - switch (spec.ColumnIndex) + var columnIndex = (int)spec.ColumnIndex; + if (isModel) { - case 0: - SortCachedAnalysis(_selectedObjectTab, pair => pair.Key, ascending, StringComparer.Ordinal); - break; - case 1: - SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.FilePaths.Count, ascending); - break; - case 2: - SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.GamePaths.Count, ascending); - break; - case 3: - SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.OriginalSize, ascending); - break; - case 4: - SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.CompressedSize, ascending); - break; - case 5 when string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal): + if (columnIndex == 0) + { + // checkbox column + } + else if (columnIndex == 1) + { + SortCachedAnalysis(_selectedObjectTab, pair => GetModelDisplayName(pair.Value), ascending, StringComparer.OrdinalIgnoreCase); + } + else if (columnIndex == 2) + { SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.Triangles, ascending); - break; + } + else if (columnIndex == 3) + { + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.OriginalSize, ascending); + } + else if (columnIndex == 4) + { + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.CompressedSize, ascending); + } + } + else + { + if (columnIndex == 0) + { + SortCachedAnalysis(_selectedObjectTab, pair => pair.Key, ascending, StringComparer.Ordinal); + } + else if (columnIndex == 1) + { + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.OriginalSize, ascending); + } + else if (columnIndex == 2) + { + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.CompressedSize, ascending); + } + else if (columnIndex == 3) + { + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.FilePaths.Count, ascending); + } + else if (columnIndex == 4) + { + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.GamePaths.Count, ascending); + } } sortSpecs.SpecsDirty = false; } - foreach (var item in fileGroup) + IEnumerable entries = fileGroup; + if (isModel && !string.IsNullOrWhiteSpace(_modelSearch)) { - using var textColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal)); - using var missingColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed); - ImGui.TableNextColumn(); - if (!item.IsComputed) - { - var warning = UiSharedService.Color(UIColors.Get("DimRed")); - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, warning); - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, warning); - } - if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal)) - { - var highlight = UiSharedService.Color(UIColors.Get("LightlessYellow")); - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, highlight); - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, highlight); - } - ImGui.TextUnformatted(item.Hash); - if (ImGui.IsItemClicked()) - { - _selectedHash = string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal) - ? string.Empty - : item.Hash; - } + var term = _modelSearch.Trim(); + entries = fileGroup.Where(entry => + entry.Hash.Contains(term, StringComparison.OrdinalIgnoreCase) + || GetModelDisplayName(entry).Contains(term, StringComparison.OrdinalIgnoreCase) + || entry.FilePaths.Exists(path => path.Contains(term, StringComparison.OrdinalIgnoreCase)) + || entry.GamePaths.Exists(path => path.Contains(term, StringComparison.OrdinalIgnoreCase))); + } - ImGui.TableNextColumn(); - ImGui.TextUnformatted(item.FilePaths.Count.ToString()); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(item.GamePaths.Count.ToString()); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize)); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize)); - - if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) + foreach (var item in entries) + { + var isSelected = string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal); + var defaultTextColor = ImGui.GetColorU32(ImGuiCol.Text); + if (isModel) { ImGui.TableNextColumn(); - ImGui.TextUnformatted(item.Triangles.ToString()); + var marked = _selectedModelKeys.Contains(item.Hash); + if (UiSharedService.CheckboxWithBorder($"##model-select-{item.Hash}", ref marked, selectionAccent, 1.5f)) + { + if (marked) + { + _selectedModelKeys.Add(item.Hash); + } + else + { + _selectedModelKeys.Remove(item.Hash); + } + } + + using (ImRaii.PushColor(ImGuiCol.Text, defaultTextColor)) + { + UiSharedService.AttachToolTip("Mark model for batch decimation."); + } + ImGui.TableNextColumn(); } + else + { + ImGui.TableNextColumn(); + } + + using var textColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), isSelected); + using var missingColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed); + if (isModel) + { + if (!item.IsComputed) + { + var warning = UiSharedService.Color(UIColors.Get("DimRed")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, warning); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, warning); + } + if (isSelected) + { + var highlight = UiSharedService.Color(UIColors.Get("LightlessYellow")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, highlight); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, highlight); + } + + var displayName = GetModelDisplayName(item); + ImGui.TextUnformatted(displayName); + using (ImRaii.PushColor(ImGuiCol.Text, defaultTextColor)) + { + UiSharedService.AttachToolTip($"Hash: {item.Hash}"); + } + if (ImGui.IsItemClicked()) + { + _selectedHash = isSelected ? string.Empty : item.Hash; + } + } + else + { + if (!item.IsComputed) + { + var warning = UiSharedService.Color(UIColors.Get("DimRed")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, warning); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, warning); + } + if (isSelected) + { + var highlight = UiSharedService.Color(UIColors.Get("LightlessYellow")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, highlight); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, highlight); + } + + ImGui.TextUnformatted(item.Hash); + if (ImGui.IsItemClicked()) + { + _selectedHash = isSelected ? string.Empty : item.Hash; + } + } + + if (isModel) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.IsComputed + ? item.Triangles.ToString("N0", CultureInfo.InvariantCulture) + : "Pending"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.IsComputed ? UiSharedService.ByteToString(item.OriginalSize) : "Pending"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.IsComputed ? UiSharedService.ByteToString(item.CompressedSize) : "Pending"); + } + else + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.IsComputed ? UiSharedService.ByteToString(item.OriginalSize) : "Pending"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.IsComputed ? UiSharedService.ByteToString(item.CompressedSize) : "Pending"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.FilePaths.Count.ToString(CultureInfo.InvariantCulture)); + if (item.FilePaths.Count > 0) + { + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, item.FilePaths)); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.GamePaths.Count.ToString(CultureInfo.InvariantCulture)); + if (item.GamePaths.Count > 0) + { + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, item.GamePaths)); + } + } + } + + static string GetModelDisplayName(CharacterAnalyzer.FileDataEntry entry) + { + var sourcePath = entry.FilePaths.FirstOrDefault(); + return string.IsNullOrWhiteSpace(sourcePath) + ? entry.Hash + : Path.GetFileName(sourcePath); } } }