Compare commits
40 Commits
2.0.2.76-D
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f598e695 | ||
|
|
861a337029 | ||
| 06f89955d3 | |||
|
|
367af2c3d0 | ||
|
|
19a238c808 | ||
|
|
c7a2b679f2 | ||
|
|
bec69074a5 | ||
| 7d86b41cee | |||
| 0185e6b534 | |||
|
|
90bf84f8eb | ||
| f27db300ec | |||
| 828be6eb5b | |||
| d039d2fd90 | |||
| e75a371475 | |||
|
|
ac711d9a43 | ||
|
|
b875e0c3a1 | ||
|
|
d6437998ac | ||
|
|
4fa9876c1c | ||
|
|
46e76bbfe6 | ||
| 9dd8e19fb7 | |||
| 5167465d28 | |||
| e8c7539770 | |||
| 54d6a0a1a4 | |||
| b57d54d69c | |||
| 8be0811b4a | |||
| 7c281926a5 | |||
| 6c7e4e6303 | |||
| e2d663cae9 | |||
| 96123d00a2 | |||
|
|
4502cadaeb | ||
| 7f33b6a4ce | |||
| 61f584f059 | |||
| 95d286f990 | |||
|
|
42d6a19db1 | ||
|
|
05f7d256d7 | ||
|
|
058ba504cb | ||
|
|
19966f3828 | ||
| 59ed03a825 | |||
| ae76efedf8 | |||
| 0e24da75d5 |
@@ -9,7 +9,8 @@ env:
|
||||
DOTNET_VERSION: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
jobs:
|
||||
tag-and-release:
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -32,16 +33,14 @@ jobs:
|
||||
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
cd /
|
||||
mkdir -p root/.xlcore/dalamud/Hooks/dev
|
||||
mkdir -p ~/.xlcore/dalamud/Hooks/dev
|
||||
curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
|
||||
unzip latest.zip -d /root/.xlcore/dalamud/Hooks/dev
|
||||
unzip latest.zip -d ~/.xlcore/dalamud/Hooks/dev
|
||||
|
||||
- name: Lets Build Lightless!
|
||||
run: |
|
||||
dotnet restore
|
||||
dotnet build --configuration Release --no-restore
|
||||
dotnet publish --configuration Release --no-build
|
||||
dotnet publish --configuration Release
|
||||
mv LightlessSync/bin/x64/Release/LightlessSync/latest.zip LightlessClient.zip
|
||||
|
||||
- name: Get version
|
||||
id: package_version
|
||||
@@ -53,19 +52,6 @@ jobs:
|
||||
run: |
|
||||
echo "Version: ${{ steps.package_version.outputs.version }}"
|
||||
|
||||
- name: Prepare Lightless Client
|
||||
run: |
|
||||
PUBLISH_PATH="/workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/publish/"
|
||||
if [ -d "$PUBLISH_PATH" ]; then
|
||||
rm -rf "$PUBLISH_PATH"
|
||||
echo "Removed $PUBLISH_PATH"
|
||||
else
|
||||
echo "$PUBLISH_PATH does not exist, nothing to remove."
|
||||
fi
|
||||
|
||||
mkdir -p output
|
||||
(cd /workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/ && zip -r $OLDPWD/output/LightlessClient.zip *)
|
||||
|
||||
- name: Create Git tag if not exists (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
@@ -162,14 +148,7 @@ jobs:
|
||||
echo "release_id=$release_id"
|
||||
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
||||
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
||||
|
||||
- name: Check asset exists
|
||||
run: |
|
||||
if [ ! -f output/LightlessClient.zip ]; then
|
||||
echo "output/LightlessClient.zip does not exist!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- name: Upload Assets to release
|
||||
env:
|
||||
RELEASE_ID: ${{ env.RELEASE_ID }}
|
||||
@@ -177,7 +156,7 @@ jobs:
|
||||
echo "Uploading to release ID: $RELEASE_ID"
|
||||
curl --fail-with-body -s -X POST \
|
||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
-F "attachment=@output/LightlessClient.zip" \
|
||||
-F "attachment=@LightlessClient.zip" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets"
|
||||
|
||||
- name: Clone plugin hosting repo
|
||||
@@ -186,7 +165,7 @@ jobs:
|
||||
cd LightlessSyncRepo
|
||||
git clone https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git
|
||||
env:
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
|
||||
- name: Update plogonmaster.json with version (master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
@@ -282,8 +261,8 @@ jobs:
|
||||
- name: Commit and push to LightlessSync
|
||||
run: |
|
||||
cd LightlessSyncRepo/LightlessSync
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
git config user.name "Gitea-Automation"
|
||||
git config user.email "aaa@aaaaaaa.aaa"
|
||||
git add .
|
||||
git diff-index --quiet HEAD || git commit -m "Update ${{ env.PLUGIN_NAME }} to ${{ steps.package_version.outputs.version }}"
|
||||
git push https://x-access-token:${{ secrets.AUTOMATION_TOKEN }}@git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git HEAD:main
|
||||
|
||||
18
LightlessCompactor/FileCache/CompactorInterfaces.cs
Normal file
18
LightlessCompactor/FileCache/CompactorInterfaces.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
public interface ICompactorContext
|
||||
{
|
||||
bool UseCompactor { get; }
|
||||
string CacheFolder { get; }
|
||||
bool IsWine { get; }
|
||||
}
|
||||
|
||||
public interface ICompactionExecutor
|
||||
{
|
||||
bool TryCompact(string filePath);
|
||||
}
|
||||
|
||||
public sealed class NoopCompactionExecutor : ICompactionExecutor
|
||||
{
|
||||
public bool TryCompact(string filePath) => false;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Compactor;
|
||||
using LightlessSync.Services.Compactor;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -20,8 +18,8 @@ public sealed partial class FileCompactor : IDisposable
|
||||
|
||||
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
||||
private readonly ILogger<FileCompactor> _logger;
|
||||
private readonly LightlessConfigService _lightlessConfigService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ICompactorContext _context;
|
||||
private readonly ICompactionExecutor _compactionExecutor;
|
||||
|
||||
private readonly Channel<string> _compactionQueue;
|
||||
private readonly CancellationTokenSource _compactionCts = new();
|
||||
@@ -59,12 +57,12 @@ public sealed partial class FileCompactor : IDisposable
|
||||
XPRESS16K = 3
|
||||
}
|
||||
|
||||
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
||||
public FileCompactor(ILogger<FileCompactor> logger, ICompactorContext context, ICompactionExecutor compactionExecutor)
|
||||
{
|
||||
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
||||
_logger = logger;
|
||||
_lightlessConfigService = lightlessConfigService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_compactionExecutor = compactionExecutor ?? throw new ArgumentNullException(nameof(compactionExecutor));
|
||||
_isWindows = OperatingSystem.IsWindows();
|
||||
|
||||
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
||||
@@ -94,7 +92,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
|
||||
//Uses an batching service for the filefrag command on Linux
|
||||
_fragBatch = new BatchFilefragService(
|
||||
useShell: _dalamudUtilService.IsWine,
|
||||
useShell: _context.IsWine,
|
||||
log: _logger,
|
||||
batchSize: 64,
|
||||
flushMs: 25,
|
||||
@@ -118,7 +116,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
var folder = _lightlessConfigService.Current.CacheFolder;
|
||||
var folder = _context.CacheFolder;
|
||||
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning))
|
||||
@@ -127,7 +125,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var files = Directory.EnumerateFiles(folder).ToArray();
|
||||
var files = Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories).ToArray();
|
||||
var total = files.Length;
|
||||
Progress = $"0/{total}";
|
||||
if (total == 0) return;
|
||||
@@ -155,7 +153,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
{
|
||||
if (compress)
|
||||
{
|
||||
if (_lightlessConfigService.Current.UseCompactor)
|
||||
if (_context.UseCompactor)
|
||||
CompactFile(file, workerId);
|
||||
}
|
||||
else
|
||||
@@ -221,19 +219,52 @@ public sealed partial class FileCompactor : IDisposable
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
||||
|
||||
if (_lightlessConfigService.Current.UseCompactor)
|
||||
if (_context.UseCompactor)
|
||||
EnqueueCompaction(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify the compactor that a file was written directly (streamed) so it can enqueue compaction.
|
||||
/// </summary>
|
||||
public void NotifyFileWritten(string filePath)
|
||||
{
|
||||
EnqueueCompaction(filePath);
|
||||
}
|
||||
|
||||
public bool TryCompactFile(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
return false;
|
||||
|
||||
if (!_context.UseCompactor || !File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
CompactFile(filePath, workerId: -1);
|
||||
return true;
|
||||
}
|
||||
catch (IOException ioEx)
|
||||
{
|
||||
_logger.LogDebug(ioEx, "File being read/written, skipping file: {file}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error compacting file: {file}", filePath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the File size for an BTRFS or NTFS file system for the given FileInfo
|
||||
/// </summary>
|
||||
/// <param name="path">Amount of blocks used in the disk</param>
|
||||
public long GetFileSizeOnDisk(FileInfo fileInfo)
|
||||
{
|
||||
var fsType = GetFilesystemType(fileInfo.FullName, _dalamudUtilService.IsWine);
|
||||
var fsType = GetFilesystemType(fileInfo.FullName, _context.IsWine);
|
||||
|
||||
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
||||
{
|
||||
(bool flowControl, long value) = GetFileSizeNTFS(fileInfo);
|
||||
if (!flowControl)
|
||||
@@ -290,7 +321,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.IsWine);
|
||||
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _context.IsWine);
|
||||
if (blockSize <= 0)
|
||||
throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}");
|
||||
|
||||
@@ -330,7 +361,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
||||
var fsType = GetFilesystemType(filePath, _context.IsWine);
|
||||
var oldSize = fi.Length;
|
||||
int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
|
||||
|
||||
@@ -346,7 +377,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
||||
{
|
||||
if (!IsWOFCompactedFile(filePath))
|
||||
{
|
||||
@@ -402,9 +433,9 @@ public sealed partial class FileCompactor : IDisposable
|
||||
private void DecompressFile(string filePath, int workerId)
|
||||
{
|
||||
_logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath);
|
||||
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
||||
var fsType = GetFilesystemType(filePath, _context.IsWine);
|
||||
|
||||
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -448,7 +479,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||
bool isWine = _context.IsWine;
|
||||
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
||||
|
||||
var opts = GetMountOptionsForPath(linuxPath);
|
||||
@@ -961,7 +992,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
if (finished != bothTasks)
|
||||
return KillProcess(proc, outTask, errTask, token);
|
||||
|
||||
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||
bool isWine = _context.IsWine;
|
||||
if (!isWine)
|
||||
{
|
||||
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
|
||||
@@ -1005,7 +1036,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
return;
|
||||
|
||||
if (!_lightlessConfigService.Current.UseCompactor)
|
||||
if (!_context.UseCompactor)
|
||||
return;
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
@@ -1017,7 +1048,7 @@ public sealed partial class FileCompactor : IDisposable
|
||||
bool enqueued = false;
|
||||
try
|
||||
{
|
||||
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||
bool isWine = _context.IsWine;
|
||||
var fsType = GetFilesystemType(filePath, isWine);
|
||||
|
||||
// If under Wine, we should skip NTFS because its not Windows but might return NTFS.
|
||||
@@ -1070,8 +1101,11 @@ public sealed partial class FileCompactor : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath))
|
||||
CompactFile(filePath, workerId);
|
||||
if (_context.UseCompactor && File.Exists(filePath))
|
||||
{
|
||||
if (!_compactionExecutor.TryCompact(filePath))
|
||||
CompactFile(filePath, workerId);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
15
LightlessCompactor/LightlessCompactor.csproj
Normal file
15
LightlessCompactor/LightlessCompactor.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
19
LightlessCompactorWorker/LightlessCompactorWorker.csproj
Normal file
19
LightlessCompactorWorker/LightlessCompactorWorker.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LightlessCompactor\LightlessCompactor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
270
LightlessCompactorWorker/Program.cs
Normal file
270
LightlessCompactorWorker/Program.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using LightlessSync.FileCache;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
|
||||
internal sealed class WorkerCompactorContext : ICompactorContext
|
||||
{
|
||||
public WorkerCompactorContext(string cacheFolder, bool isWine)
|
||||
{
|
||||
CacheFolder = cacheFolder;
|
||||
IsWine = isWine;
|
||||
}
|
||||
|
||||
public bool UseCompactor => true;
|
||||
public string CacheFolder { get; }
|
||||
public bool IsWine { get; }
|
||||
}
|
||||
|
||||
internal sealed class WorkerOptions
|
||||
{
|
||||
public string? FilePath { get; init; }
|
||||
public bool IsWine { get; init; }
|
||||
public string CacheFolder { get; init; } = string.Empty;
|
||||
public LogLevel LogLevel { get; init; } = LogLevel.Information;
|
||||
public string PipeName { get; init; } = "LightlessCompactor";
|
||||
public int? ParentProcessId { get; init; }
|
||||
}
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
var options = ParseOptions(args, out var error);
|
||||
if (options is null)
|
||||
{
|
||||
Console.Error.WriteLine(error ?? "Invalid arguments.");
|
||||
Console.Error.WriteLine("Usage: LightlessCompactorWorker --file <path> [--wine] [--cache-folder <path>] [--verbose]");
|
||||
Console.Error.WriteLine(" or: LightlessCompactorWorker --pipe <name> [--wine] [--parent <pid>] [--verbose]");
|
||||
return 2;
|
||||
}
|
||||
|
||||
TrySetLowPriority();
|
||||
|
||||
using var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.SetMinimumLevel(options.LogLevel);
|
||||
builder.AddSimpleConsole(o =>
|
||||
{
|
||||
o.SingleLine = true;
|
||||
o.TimestampFormat = "HH:mm:ss.fff ";
|
||||
});
|
||||
});
|
||||
|
||||
var logger = loggerFactory.CreateLogger<FileCompactor>();
|
||||
var context = new WorkerCompactorContext(options.CacheFolder, options.IsWine);
|
||||
|
||||
using var compactor = new FileCompactor(logger, context, new NoopCompactionExecutor());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.FilePath))
|
||||
{
|
||||
var success = compactor.TryCompactFile(options.FilePath!);
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
var serverLogger = loggerFactory.CreateLogger("CompactorWorker");
|
||||
return await RunServerAsync(compactor, options, serverLogger).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<int> RunServerAsync(FileCompactor compactor, WorkerOptions options, ILogger serverLogger)
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
var token = cts.Token;
|
||||
|
||||
if (options.ParentProcessId.HasValue)
|
||||
{
|
||||
_ = Task.Run(() => MonitorParent(options.ParentProcessId.Value, cts));
|
||||
}
|
||||
|
||||
serverLogger.LogInformation("Compactor worker listening on pipe {pipe}", options.PipeName);
|
||||
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var server = new NamedPipeServerStream(
|
||||
options.PipeName,
|
||||
PipeDirection.InOut,
|
||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
try
|
||||
{
|
||||
await server.WaitForConnectionAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
server.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => HandleClientAsync(server, compactor, cts));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// shutdown requested
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
serverLogger.LogWarning(ex, "Compactor worker terminated unexpectedly.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async Task HandleClientAsync(NamedPipeServerStream pipe, FileCompactor compactor, CancellationTokenSource shutdownCts)
|
||||
{
|
||||
await using var _ = pipe;
|
||||
using var reader = new StreamReader(pipe);
|
||||
using var writer = new StreamWriter(pipe) { AutoFlush = true };
|
||||
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return;
|
||||
|
||||
CompactorRequest? request = null;
|
||||
try
|
||||
{
|
||||
request = JsonSerializer.Deserialize<CompactorRequest>(line);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
CompactorResponse response;
|
||||
if (request is null)
|
||||
{
|
||||
response = new CompactorResponse { Success = false, Error = "Invalid request." };
|
||||
}
|
||||
else if (string.Equals(request.Type, "shutdown", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shutdownCts.Cancel();
|
||||
response = new CompactorResponse { Success = true };
|
||||
}
|
||||
else if (string.Equals(request.Type, "compact", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var success = compactor.TryCompactFile(request.Path ?? string.Empty);
|
||||
response = new CompactorResponse { Success = success };
|
||||
}
|
||||
else
|
||||
{
|
||||
response = new CompactorResponse { Success = false, Error = "Unknown request type." };
|
||||
}
|
||||
|
||||
await writer.WriteLineAsync(JsonSerializer.Serialize(response)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void MonitorParent(int parentPid, CancellationTokenSource shutdownCts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parent = Process.GetProcessById(parentPid);
|
||||
parent.WaitForExit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// parent missing
|
||||
}
|
||||
finally
|
||||
{
|
||||
shutdownCts.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static WorkerOptions? ParseOptions(string[] args, out string? error)
|
||||
{
|
||||
string? filePath = null;
|
||||
bool isWine = false;
|
||||
string cacheFolder = string.Empty;
|
||||
var logLevel = LogLevel.Information;
|
||||
string pipeName = "LightlessCompactor";
|
||||
int? parentPid = null;
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
switch (arg)
|
||||
{
|
||||
case "--file":
|
||||
if (i + 1 >= args.Length)
|
||||
{
|
||||
error = "Missing value for --file.";
|
||||
return null;
|
||||
}
|
||||
filePath = args[++i];
|
||||
break;
|
||||
case "--cache-folder":
|
||||
if (i + 1 >= args.Length)
|
||||
{
|
||||
error = "Missing value for --cache-folder.";
|
||||
return null;
|
||||
}
|
||||
cacheFolder = args[++i];
|
||||
break;
|
||||
case "--pipe":
|
||||
if (i + 1 >= args.Length)
|
||||
{
|
||||
error = "Missing value for --pipe.";
|
||||
return null;
|
||||
}
|
||||
pipeName = args[++i];
|
||||
break;
|
||||
case "--parent":
|
||||
if (i + 1 >= args.Length || !int.TryParse(args[++i], out var pid))
|
||||
{
|
||||
error = "Invalid value for --parent.";
|
||||
return null;
|
||||
}
|
||||
parentPid = pid;
|
||||
break;
|
||||
case "--wine":
|
||||
isWine = true;
|
||||
break;
|
||||
case "--verbose":
|
||||
logLevel = LogLevel.Trace;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
error = null;
|
||||
return new WorkerOptions
|
||||
{
|
||||
FilePath = filePath,
|
||||
IsWine = isWine,
|
||||
CacheFolder = cacheFolder,
|
||||
LogLevel = logLevel,
|
||||
PipeName = pipeName,
|
||||
ParentProcessId = parentPid
|
||||
};
|
||||
}
|
||||
|
||||
private static void TrySetLowPriority()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CompactorRequest
|
||||
{
|
||||
public string Type { get; init; } = "compact";
|
||||
public string? Path { get; init; }
|
||||
}
|
||||
|
||||
private sealed class CompactorResponse
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterG
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessCompactor", "LightlessCompactor\LightlessCompactor.csproj", "{01F31917-9F1E-426D-BDAE-17268CBF9523}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessCompactorWorker", "LightlessCompactorWorker\LightlessCompactorWorker.csproj", "{72BE3664-CD0E-4DA4-B040-91338A2798E0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -116,6 +120,30 @@ Global
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x64.Build.0 = Release|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x86.Build.0 = Release|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
241
LightlessSync/FileCache/ExternalCompactionExecutor.cs
Normal file
241
LightlessSync/FileCache/ExternalCompactionExecutor.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
internal sealed class ExternalCompactionExecutor : ICompactionExecutor, IDisposable
|
||||
{
|
||||
private readonly ILogger<ExternalCompactionExecutor> _logger;
|
||||
private readonly ICompactorContext _context;
|
||||
private readonly TimeSpan _timeout = TimeSpan.FromMinutes(5);
|
||||
private readonly string _pipeName;
|
||||
private Process? _workerProcess;
|
||||
private bool _disposed;
|
||||
private readonly object _sync = new();
|
||||
|
||||
public ExternalCompactionExecutor(ILogger<ExternalCompactionExecutor> logger, ICompactorContext context)
|
||||
{
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
_pipeName = $"LightlessCompactor-{Environment.ProcessId}";
|
||||
}
|
||||
|
||||
public bool TryCompact(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
if (!EnsureWorkerRunning())
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var request = new CompactorRequest
|
||||
{
|
||||
Type = "compact",
|
||||
Path = filePath
|
||||
};
|
||||
|
||||
return SendRequest(request, out var response) && response?.Success == true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "External compactor failed for {file}", filePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
try
|
||||
{
|
||||
SendRequest(new CompactorRequest { Type = "shutdown" }, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_workerProcess is null)
|
||||
return;
|
||||
|
||||
TryKill(_workerProcess);
|
||||
_workerProcess.Dispose();
|
||||
_workerProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnsureWorkerRunning()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_workerProcess is { HasExited: false })
|
||||
return true;
|
||||
|
||||
_workerProcess?.Dispose();
|
||||
_workerProcess = null;
|
||||
|
||||
var workerPath = ResolveWorkerPath();
|
||||
if (string.IsNullOrEmpty(workerPath))
|
||||
return false;
|
||||
|
||||
var args = BuildArguments();
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = workerPath,
|
||||
Arguments = args,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
var process = new Process { StartInfo = startInfo };
|
||||
if (!process.Start())
|
||||
return false;
|
||||
|
||||
TrySetLowPriority(process);
|
||||
_ = DrainAsync(process.StandardOutput, "stdout");
|
||||
_ = DrainAsync(process.StandardError, "stderr");
|
||||
|
||||
_workerProcess = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool SendRequest(CompactorRequest request, out CompactorResponse? response)
|
||||
{
|
||||
response = null;
|
||||
using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
|
||||
try
|
||||
{
|
||||
pipe.Connect((int)_timeout.TotalMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Compactor pipe connection failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
using var writer = new StreamWriter(pipe) { AutoFlush = true };
|
||||
using var reader = new StreamReader(pipe);
|
||||
|
||||
var payload = JsonSerializer.Serialize(request);
|
||||
writer.WriteLine(payload);
|
||||
|
||||
var readTask = reader.ReadLineAsync();
|
||||
if (!readTask.Wait(_timeout))
|
||||
{
|
||||
_logger.LogWarning("Compactor pipe timed out waiting for response.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var line = readTask.Result;
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
response = JsonSerializer.Deserialize<CompactorResponse>(line);
|
||||
return response is not null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to parse compactor response.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string? ResolveWorkerPath()
|
||||
{
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var exeName = OperatingSystem.IsWindows() || _context.IsWine
|
||||
? "LightlessCompactorWorker.exe"
|
||||
: "LightlessCompactorWorker";
|
||||
var path = Path.Combine(baseDir, exeName);
|
||||
return File.Exists(path) ? path : null;
|
||||
}
|
||||
|
||||
private string BuildArguments()
|
||||
{
|
||||
var args = new List<string> { "--pipe", Quote(_pipeName), "--parent", Environment.ProcessId.ToString() };
|
||||
if (_context.IsWine)
|
||||
args.Add("--wine");
|
||||
return string.Join(' ', args);
|
||||
}
|
||||
|
||||
private static string Quote(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return "\"\"";
|
||||
|
||||
if (!value.Contains('"', StringComparison.Ordinal))
|
||||
return "\"" + value + "\"";
|
||||
|
||||
return "\"" + value.Replace("\"", "\\\"", StringComparison.Ordinal) + "\"";
|
||||
}
|
||||
|
||||
private static void TrySetLowPriority(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DrainAsync(StreamReader reader, string label)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.LogTrace("Compactor {label}: {line}", label, line);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryKill(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CompactorRequest
|
||||
{
|
||||
public string Type { get; init; } = "compact";
|
||||
public string? Path { get; init; }
|
||||
}
|
||||
|
||||
private sealed class CompactorResponse
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,35 @@ public sealed class FileCacheManager : IHostedService
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetHashFromFileName(FileInfo fileInfo, out string hash)
|
||||
{
|
||||
hash = Path.GetFileNameWithoutExtension(fileInfo.Name);
|
||||
if (string.IsNullOrWhiteSpace(hash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hash.Length is not (40 or 64))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
var c = hash[i];
|
||||
var isHex = (c >= '0' && c <= '9')
|
||||
|| (c >= 'a' && c <= 'f')
|
||||
|| (c >= 'A' && c <= 'F');
|
||||
if (!isHex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
hash = hash.ToUpperInvariant();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||
|
||||
private static bool TryParseVersionHeader(string? line, out int version)
|
||||
@@ -288,6 +317,11 @@ public sealed class FileCacheManager : IHostedService
|
||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||
if (TryGetHashFromFileName(fi, out var hash))
|
||||
{
|
||||
return CreateCacheEntryWithKnownHash(fi.FullName, hash);
|
||||
}
|
||||
|
||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||
}
|
||||
|
||||
|
||||
20
LightlessSync/FileCache/PluginCompactorContext.cs
Normal file
20
LightlessSync/FileCache/PluginCompactorContext.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
internal sealed class PluginCompactorContext : ICompactorContext
|
||||
{
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
|
||||
public PluginCompactorContext(LightlessConfigService configService, DalamudUtilService dalamudUtilService)
|
||||
{
|
||||
_configService = configService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
}
|
||||
|
||||
public bool UseCompactor => _configService.Current.UseCompactor;
|
||||
public string CacheFolder => _configService.Current.CacheFolder;
|
||||
public bool IsWine => _dalamudUtilService.IsWine;
|
||||
}
|
||||
@@ -25,7 +25,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
private readonly object _ownedHandlerLock = new();
|
||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
||||
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||
private readonly string[] _handledFileTypesWithRecording;
|
||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||
private readonly object _playerRelatedLock = new();
|
||||
private readonly ConcurrentDictionary<nint, GameObjectHandler> _playerRelatedByAddress = new();
|
||||
@@ -42,8 +41,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_handledFileTypesWithRecording = _handledRecordingFileTypes.Concat(_handledFileTypes).ToArray();
|
||||
|
||||
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||
@@ -523,46 +520,51 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||
{
|
||||
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||
var gameObjectAddress = msg.GameObject;
|
||||
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||
{
|
||||
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
|
||||
{
|
||||
objectKind = ownedKind;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var gamePath = NormalizeGamePath(msg.GamePath);
|
||||
if (string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var filePath = msg.FilePath;
|
||||
|
||||
// ignore files already processed this frame
|
||||
if (_cachedHandledPaths.Contains(gamePath)) return;
|
||||
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
if (!_cachedHandledPaths.Add(gamePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_cachedHandledPaths.Add(gamePath);
|
||||
}
|
||||
|
||||
// replace individual mtrl stuff
|
||||
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filePath = filePath.Split("|")[2];
|
||||
}
|
||||
// replace filepath
|
||||
filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// ignore files that are the same
|
||||
var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore files to not handle
|
||||
var handledTypes = IsTransientRecording ? _handledFileTypesWithRecording : _handledFileTypes;
|
||||
if (!HasHandledFileType(gamePath, handledTypes))
|
||||
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
|
||||
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
_cachedHandledPaths.Add(gamePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var filePath = NormalizeFilePath(msg.FilePath);
|
||||
|
||||
// ignore files that are the same
|
||||
if (string.Equals(filePath, gamePath, StringComparison.Ordinal))
|
||||
// ignore files not belonging to anything player related
|
||||
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||
{
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
_cachedHandledPaths.Add(gamePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -577,12 +579,13 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_playerRelatedByAddress.TryGetValue(gameObjectAddress, out var owner);
|
||||
bool alreadyTransient = false;
|
||||
|
||||
bool transientContains = transientResources.Contains(gamePath);
|
||||
bool semiTransientContains = SemiTransientResources.Values.Any(value => value.Contains(gamePath));
|
||||
bool transientContains = transientResources.Contains(replacedGamePath);
|
||||
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value)
|
||||
.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
||||
if (transientContains || semiTransientContains)
|
||||
{
|
||||
if (!IsTransientRecording)
|
||||
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", gamePath, filePath,
|
||||
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath,
|
||||
transientContains, semiTransientContains);
|
||||
alreadyTransient = true;
|
||||
}
|
||||
@@ -590,10 +593,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (!IsTransientRecording)
|
||||
{
|
||||
bool isAdded = transientResources.Add(gamePath);
|
||||
bool isAdded = transientResources.Add(replacedGamePath);
|
||||
if (isAdded)
|
||||
{
|
||||
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", gamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||
SendTransients(gameObjectAddress, objectKind);
|
||||
}
|
||||
}
|
||||
@@ -601,7 +604,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
if (owner != null && IsTransientRecording)
|
||||
{
|
||||
_recordedTransients.Add(new TransientRecord(owner, gamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,4 +703,4 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
public bool AddTransient { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace Lifestream.Enums;
|
||||
|
||||
public enum TerritoryTypeIdHousing
|
||||
{
|
||||
None = -1,
|
||||
|
||||
// Mist (Limsa Lominsa)
|
||||
Mist = 339,
|
||||
MistSmall = 282,
|
||||
MistMedium = 283,
|
||||
MistLarge = 284,
|
||||
MistFCRoom = 384,
|
||||
MistFCWorkshop = 423,
|
||||
MistApartment = 608,
|
||||
|
||||
// Lavender Beds (Gridania)
|
||||
Lavender = 340,
|
||||
LavenderSmall = 342,
|
||||
LavenderMedium = 343,
|
||||
LavenderLarge = 344,
|
||||
LavenderFCRoom = 385,
|
||||
LavenderFCWorkshop = 425,
|
||||
LavenderApartment = 609,
|
||||
|
||||
// Goblet (Ul'dah)
|
||||
Goblet = 341,
|
||||
GobletSmall = 345,
|
||||
GobletMedium = 346,
|
||||
GobletLarge = 347,
|
||||
GobletFCRoom = 386,
|
||||
GobletFCWorkshop = 424,
|
||||
GobletApartment = 610,
|
||||
|
||||
// Shirogane (Kugane)
|
||||
Shirogane = 641,
|
||||
ShiroganeSmall = 649,
|
||||
ShiroganeMedium = 650,
|
||||
ShiroganeLarge = 651,
|
||||
ShiroganeFCRoom = 652,
|
||||
ShiroganeFCWorkshop = 653,
|
||||
ShiroganeApartment = 655,
|
||||
|
||||
// Empyreum (Ishgard)
|
||||
Empyream = 979,
|
||||
EmpyreamSmall = 980,
|
||||
EmpyreamMedium = 981,
|
||||
EmpyreamLarge = 982,
|
||||
EmpyreamFCRoom = 983,
|
||||
EmpyreamFCWorkshop = 984,
|
||||
EmpyreamApartment = 999,
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using LightlessSync.Interop.Ipc.Penumbra;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
@@ -36,8 +35,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
RedrawManager redrawManager,
|
||||
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||
RedrawManager redrawManager) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||
{
|
||||
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
||||
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
||||
@@ -46,7 +44,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
||||
|
||||
_collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
|
||||
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService));
|
||||
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator));
|
||||
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
|
||||
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
|
||||
|
||||
@@ -104,8 +102,11 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
||||
|
||||
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token);
|
||||
public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType)
|
||||
=> _redraw.RequestImmediateRedraw(objectIndex, redrawType);
|
||||
|
||||
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token, bool requestRedraw = true)
|
||||
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token, requestRedraw);
|
||||
|
||||
public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
||||
=> _textures.ConvertTextureFileDirectAsync(job, token);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
@@ -16,10 +14,6 @@ public sealed class PenumbraCollections : PenumbraBase
|
||||
private readonly DeleteTemporaryCollection _removeTemporaryCollection;
|
||||
private readonly AddTemporaryMod _addTemporaryMod;
|
||||
private readonly RemoveTemporaryMod _removeTemporaryMod;
|
||||
private readonly GetCollections _getCollections;
|
||||
private readonly ConcurrentDictionary<Guid, string> _activeTemporaryCollections = new();
|
||||
|
||||
private int _cleanupScheduled;
|
||||
|
||||
public PenumbraCollections(
|
||||
ILogger logger,
|
||||
@@ -32,7 +26,6 @@ public sealed class PenumbraCollections : PenumbraBase
|
||||
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
|
||||
_addTemporaryMod = new AddTemporaryMod(pluginInterface);
|
||||
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
|
||||
_getCollections = new GetCollections(pluginInterface);
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Collections";
|
||||
@@ -62,16 +55,11 @@ public sealed class PenumbraCollections : PenumbraBase
|
||||
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var name = $"Lightless_{uid}";
|
||||
_createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
||||
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId);
|
||||
var createResult = _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
||||
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}, Result: {Result}", name, tempCollectionId, createResult);
|
||||
return (tempCollectionId, name);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (collectionId != Guid.Empty)
|
||||
{
|
||||
_activeTemporaryCollections[collectionId] = collectionName;
|
||||
}
|
||||
|
||||
return collectionId;
|
||||
}
|
||||
|
||||
@@ -89,7 +77,6 @@ public sealed class PenumbraCollections : PenumbraBase
|
||||
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||
}
|
||||
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
||||
@@ -131,67 +118,5 @@ public sealed class PenumbraCollections : PenumbraBase
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
if (current == IpcConnectionState.Available)
|
||||
{
|
||||
ScheduleCleanup();
|
||||
}
|
||||
else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available)
|
||||
{
|
||||
Interlocked.Exchange(ref _cleanupScheduled, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleCleanup()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(CleanupTemporaryCollectionsAsync);
|
||||
}
|
||||
|
||||
private async Task CleanupTemporaryCollectionsAsync()
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false);
|
||||
foreach (var (collectionId, name) in collections)
|
||||
{
|
||||
if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
||||
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId);
|
||||
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
||||
return result;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (deleteResult == PenumbraApiEc.Success)
|
||||
{
|
||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLightlessCollectionName(string? name)
|
||||
=> !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
@@ -12,7 +13,6 @@ namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraResource : PenumbraBase
|
||||
{
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
||||
private readonly ResolveGameObjectPath _resolveGameObjectPath;
|
||||
private readonly ReverseResolveGameObjectPath _reverseResolveGameObjectPath;
|
||||
@@ -24,10 +24,8 @@ public sealed class PenumbraResource : PenumbraBase
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
ActorObjectService actorObjectService) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
||||
_resolveGameObjectPath = new ResolveGameObjectPath(pluginInterface);
|
||||
_reverseResolveGameObjectPath = new ReverseResolveGameObjectPath(pluginInterface);
|
||||
@@ -45,17 +43,33 @@ public sealed class PenumbraResource : PenumbraBase
|
||||
return null;
|
||||
}
|
||||
|
||||
return await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
var requestId = Guid.NewGuid();
|
||||
var totalTimer = Stopwatch.StartNew();
|
||||
logger.LogTrace("[{requestId}] Requesting Penumbra.GetGameObjectResourcePaths for {handler}", requestId, handler);
|
||||
|
||||
var result = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (idx == null)
|
||||
{
|
||||
logger.LogTrace("[{requestId}] GetGameObjectResourcePaths aborted (missing object index) for {handler}", requestId, handler);
|
||||
return null;
|
||||
}
|
||||
|
||||
return _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
||||
logger.LogTrace("[{requestId}] Invoking Penumbra.GetGameObjectResourcePaths for index {index}", requestId, idx.Value);
|
||||
var invokeTimer = Stopwatch.StartNew();
|
||||
var data = _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
||||
invokeTimer.Stop();
|
||||
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths returned {count} entries in {elapsedMs}ms",
|
||||
requestId, data?.Count ?? 0, invokeTimer.ElapsedMilliseconds);
|
||||
return data;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
totalTimer.Stop();
|
||||
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths finished in {elapsedMs}ms (null: {isNull})",
|
||||
requestId, totalTimer.ElapsedMilliseconds, result is null);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetMetaManipulations()
|
||||
@@ -79,22 +93,10 @@ public sealed class PenumbraResource : PenumbraBase
|
||||
|
||||
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
|
||||
{
|
||||
if (ptr == nint.Zero)
|
||||
if (ptr != nint.Zero && string.Compare(gamePath, resolvedPath, ignoreCase: true, CultureInfo.InvariantCulture) != 0)
|
||||
{
|
||||
return;
|
||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
||||
}
|
||||
|
||||
if (!_actorObjectService.TryGetOwnedKind(ptr, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Compare(gamePath, resolvedPath, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
||||
|
||||
public override string Name => "Penumbra.Textures";
|
||||
|
||||
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token, bool requestRedraw)
|
||||
{
|
||||
if (!IsAvailable || jobs.Count == 0)
|
||||
{
|
||||
@@ -57,7 +57,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync)));
|
||||
}
|
||||
|
||||
if (completedJobs > 0 && !token.IsCancellationRequested)
|
||||
if (requestRedraw && completedJobs > 0 && !token.IsCancellationRequested)
|
||||
{
|
||||
await DalamudUtil.RunOnFrameworkThread(async () =>
|
||||
{
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using LightlessSync.WebAPI;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
||||
ServerConfigService serverConfigService) : IHostedService
|
||||
ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
|
||||
LightlessConfigService lightlessConfigService) : IHostedService
|
||||
{
|
||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||
|
||||
@@ -51,6 +57,8 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
||||
serverConfigService.Current.Version = 2;
|
||||
serverConfigService.Save();
|
||||
}
|
||||
|
||||
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -63,4 +71,273 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void MigrateTempCollectionConfig(TempCollectionConfigService tempCollectionConfigService, LightlessConfigService lightlessConfigService)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
TempCollectionConfig tempConfig = tempCollectionConfigService.Current;
|
||||
var tempChanged = false;
|
||||
var tempNeedsSave = false;
|
||||
|
||||
if (TryReadTempCollectionData(lightlessConfigService.ConfigurationPath, out var root, out var ids, out var entries))
|
||||
{
|
||||
tempChanged |= MergeTempCollectionData(tempConfig, ids, entries, now);
|
||||
var removed = root.Remove("OrphanableTempCollections");
|
||||
removed |= root.Remove("OrphanableTempCollectionEntries");
|
||||
if (removed)
|
||||
{
|
||||
try
|
||||
{
|
||||
string updatedJson = root.ToJsonString(new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
File.WriteAllText(lightlessConfigService.ConfigurationPath, updatedJson);
|
||||
lightlessConfigService.UpdateLastWriteTime();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to rewrite {config} after temp collection migration", lightlessConfigService.ConfigurationPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.Count > 0 || entries.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Migrated {ids} temp collection ids and {entries} entries to {configName}",
|
||||
ids.Count, entries.Count, tempCollectionConfigService.ConfigurationName);
|
||||
}
|
||||
}
|
||||
|
||||
if (TryReadTempCollectionData(tempCollectionConfigService.ConfigurationPath, out var tempRoot, out var tempIds, out var tempEntries))
|
||||
{
|
||||
tempChanged |= MergeTempCollectionData(tempConfig, tempIds, tempEntries, now);
|
||||
if (tempRoot.Remove("OrphanableTempCollections"))
|
||||
{
|
||||
tempNeedsSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (tempChanged || tempNeedsSave)
|
||||
{
|
||||
tempCollectionConfigService.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReadTempCollectionData(string configPath, out JsonObject root, out HashSet<Guid> ids, out List<OrphanableTempCollectionEntry> entries)
|
||||
{
|
||||
root = new JsonObject();
|
||||
ids = [];
|
||||
entries = [];
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
root = JsonNode.Parse(File.ReadAllText(configPath)) as JsonObject ?? new JsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read temp collection data from {config}", configPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
root.TryGetPropertyValue("OrphanableTempCollections", out JsonNode? idsNode);
|
||||
root.TryGetPropertyValue("OrphanableTempCollectionEntries", out JsonNode? entriesNode);
|
||||
|
||||
if (idsNode == null && entriesNode == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ids = ParseGuidSet(idsNode);
|
||||
entries = ParseEntries(entriesNode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static HashSet<Guid> ParseGuidSet(JsonNode? node)
|
||||
{
|
||||
HashSet<Guid> ids = [];
|
||||
if (node is not JsonArray array)
|
||||
{
|
||||
return ids;
|
||||
}
|
||||
|
||||
foreach (JsonNode? item in array)
|
||||
{
|
||||
Guid id = ParseGuid(item);
|
||||
if (id != Guid.Empty)
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static List<OrphanableTempCollectionEntry> ParseEntries(JsonNode? node)
|
||||
{
|
||||
List<OrphanableTempCollectionEntry> entries = [];
|
||||
if (node is not JsonArray array)
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
foreach (JsonNode? item in array)
|
||||
{
|
||||
if (item is not JsonObject obj)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Guid id = ParseGuid(obj["Id"]);
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DateTime registeredAtUtc = DateTime.MinValue;
|
||||
if (TryParseDateTime(obj["RegisteredAtUtc"], out DateTime parsed))
|
||||
{
|
||||
registeredAtUtc = parsed;
|
||||
}
|
||||
|
||||
entries.Add(new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = id,
|
||||
RegisteredAtUtc = registeredAtUtc
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static Guid ParseGuid(JsonNode? node)
|
||||
{
|
||||
if (node is JsonValue value)
|
||||
{
|
||||
if (value.TryGetValue<string>(out string? stringValue) && Guid.TryParse(stringValue, out Guid parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
private static bool TryParseDateTime(JsonNode? node, out DateTime value)
|
||||
{
|
||||
value = DateTime.MinValue;
|
||||
if (node is not JsonValue val)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (val.TryGetValue<DateTime>(out DateTime direct))
|
||||
{
|
||||
value = direct;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (val.TryGetValue<string>(out string? stringValue)
|
||||
&& DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsed))
|
||||
{
|
||||
value = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool MergeTempCollectionData(TempCollectionConfig config, HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||
{
|
||||
bool changed = false;
|
||||
Dictionary<Guid, OrphanableTempCollectionEntry> entryLookup = new();
|
||||
for (var i = config.OrphanableTempCollectionEntries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var entry = config.OrphanableTempCollectionEntries[i];
|
||||
if (entry.Id == Guid.Empty)
|
||||
{
|
||||
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entryLookup.TryGetValue(entry.Id, out var existing))
|
||||
{
|
||||
if (entry.RegisteredAtUtc != DateTime.MinValue
|
||||
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
||||
{
|
||||
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
entryLookup[entry.Id] = entry;
|
||||
}
|
||||
|
||||
foreach (OrphanableTempCollectionEntry entry in entries)
|
||||
{
|
||||
if (entry.Id == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entryLookup.TryGetValue(entry.Id, out OrphanableTempCollectionEntry? existing))
|
||||
{
|
||||
var added = new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = entry.Id,
|
||||
RegisteredAtUtc = entry.RegisteredAtUtc
|
||||
};
|
||||
config.OrphanableTempCollectionEntries.Add(added);
|
||||
entryLookup[entry.Id] = added;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.RegisteredAtUtc != DateTime.MinValue
|
||||
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
||||
{
|
||||
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Guid id in ids)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entryLookup.TryGetValue(id, out OrphanableTempCollectionEntry? existing))
|
||||
{
|
||||
var added = new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = id,
|
||||
RegisteredAtUtc = now
|
||||
};
|
||||
config.OrphanableTempCollectionEntries.Add(added);
|
||||
entryLookup[id] = added;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing.RegisteredAtUtc == DateTime.MinValue)
|
||||
{
|
||||
existing.RegisteredAtUtc = now;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,37 +72,41 @@ public class ConfigurationSaveService : IHostedService
|
||||
{
|
||||
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
|
||||
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
|
||||
var isTempCollections = string.Equals(config.ConfigurationName, TempCollectionConfigService.ConfigName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
try
|
||||
if (!isTempCollections)
|
||||
{
|
||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||
if (!Directory.Exists(configBackupFolder))
|
||||
Directory.CreateDirectory(configBackupFolder);
|
||||
|
||||
var configNameSplit = config.ConfigurationName.Split(".");
|
||||
var existingConfigs = Directory.EnumerateFiles(
|
||||
configBackupFolder,
|
||||
configNameSplit[0] + "*")
|
||||
.Select(c => new FileInfo(c))
|
||||
.OrderByDescending(c => c.LastWriteTime).ToList();
|
||||
if (existingConfigs.Skip(10).Any())
|
||||
try
|
||||
{
|
||||
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||
{
|
||||
oldBak.Delete();
|
||||
}
|
||||
}
|
||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||
if (!Directory.Exists(configBackupFolder))
|
||||
Directory.CreateDirectory(configBackupFolder);
|
||||
|
||||
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
||||
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
||||
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
||||
FileInfo fi = new(backupPath);
|
||||
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ignore if file cannot be backupped
|
||||
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||
var configNameSplit = config.ConfigurationName.Split(".");
|
||||
var existingConfigs = Directory.EnumerateFiles(
|
||||
configBackupFolder,
|
||||
configNameSplit[0] + "*")
|
||||
.Select(c => new FileInfo(c))
|
||||
.OrderByDescending(c => c.LastWriteTime).ToList();
|
||||
if (existingConfigs.Skip(10).Any())
|
||||
{
|
||||
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||
{
|
||||
oldBak.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
||||
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
||||
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
||||
FileInfo fi = new(backupPath);
|
||||
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ignore if file cannot be backupped
|
||||
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||
}
|
||||
}
|
||||
|
||||
var temp = config.ConfigurationPath + ".tmp";
|
||||
@@ -110,7 +114,7 @@ public class ConfigurationSaveService : IHostedService
|
||||
{
|
||||
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
||||
{
|
||||
WriteIndented = true
|
||||
WriteIndented = !isTempCollections
|
||||
})).ConfigureAwait(false);
|
||||
File.Move(temp, config.ConfigurationPath, true);
|
||||
config.UpdateLastWriteTime();
|
||||
|
||||
@@ -12,6 +12,9 @@ public sealed class ChatConfig : ILightlessConfiguration
|
||||
public bool ShowMessageTimestamps { get; set; } = true;
|
||||
public bool ShowNotesInSyncshellChat { get; set; } = true;
|
||||
public bool EnableAnimatedEmotes { get; set; } = true;
|
||||
public float EmoteScale { get; set; } = 1.5f;
|
||||
public bool EnableMentionNotifications { get; set; } = true;
|
||||
public bool AutoOpenChatOnNewMessage { get; set; } = false;
|
||||
public float ChatWindowOpacity { get; set; } = .97f;
|
||||
public bool FadeWhenUnfocused { get; set; } = false;
|
||||
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
||||
@@ -23,6 +26,9 @@ public sealed class ChatConfig : ILightlessConfiguration
|
||||
public bool ShowWhenUiHidden { get; set; } = true;
|
||||
public bool ShowInCutscenes { get; set; } = true;
|
||||
public bool ShowInGpose { get; set; } = true;
|
||||
public bool PersistSyncshellHistory { get; set; } = false;
|
||||
public List<string> ChannelOrder { get; set; } = new();
|
||||
public Dictionary<string, bool> HiddenChannels { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, string> SyncshellChannelHistory { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
||||
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
||||
public bool UseLightlessRedesign { get; set; } = true;
|
||||
public bool ShowUiWhenUiHidden { get; set; } = true;
|
||||
public bool ShowUiInGpose { get; set; } = true;
|
||||
public bool EnableRightClickMenus { get; set; } = true;
|
||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||
public string ExportFolder { get; set; } = string.Empty;
|
||||
@@ -158,9 +160,7 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
public string LastSeenVersion { get; set; } = string.Empty;
|
||||
public bool EnableParticleEffects { get; set; } = true;
|
||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe;
|
||||
public bool AnimationAllowOneBasedShift { get; set; } = true;
|
||||
|
||||
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
|
||||
public bool AnimationAllowOneBasedShift { get; set; } = false;
|
||||
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>Default triangle threshold for batch decimation (0 = no threshold).</summary>
|
||||
public const int BatchTriangleThreshold = 0;
|
||||
|
||||
/// <summary>Default target triangle ratio for batch decimation.</summary>
|
||||
public const double BatchTargetRatio = 0.8;
|
||||
|
||||
/// <summary>Default tangent normalization toggle for batch decimation.</summary>
|
||||
public const bool BatchNormalizeTangents = true;
|
||||
|
||||
/// <summary>Default body collision guard toggle for batch decimation.</summary>
|
||||
public const bool BatchAvoidBodyIntersection = true;
|
||||
|
||||
/// <summary>Default display for the batch decimation warning overlay.</summary>
|
||||
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
|
||||
{
|
||||
/// <summary>Minimum triangles per connected component before skipping decimation.</summary>
|
||||
public const int DefaultMinComponentTriangles = 6;
|
||||
|
||||
/// <summary>Average-edge multiplier used to cap collapses.</summary>
|
||||
public const float DefaultMaxCollapseEdgeLengthFactor = 1.25f;
|
||||
|
||||
/// <summary>Maximum normal deviation (degrees) allowed for a collapse.</summary>
|
||||
public const float DefaultNormalSimilarityThresholdDegrees = 60f;
|
||||
|
||||
/// <summary>Minimum bone-weight overlap required to allow a collapse.</summary>
|
||||
public const float DefaultBoneWeightSimilarityThreshold = 0.85f;
|
||||
|
||||
/// <summary>UV similarity threshold to protect seams.</summary>
|
||||
public const float DefaultUvSimilarityThreshold = 0.02f;
|
||||
|
||||
/// <summary>UV seam cosine threshold for blocking seam collapses.</summary>
|
||||
public const float DefaultUvSeamAngleCos = 0.99f;
|
||||
|
||||
/// <summary>Whether to block UV seam vertices from collapsing.</summary>
|
||||
public const bool DefaultBlockUvSeamVertices = true;
|
||||
|
||||
/// <summary>Whether to allow collapses on boundary edges.</summary>
|
||||
public const bool DefaultAllowBoundaryCollapses = false;
|
||||
|
||||
/// <summary>Body collision distance factor for the primary pass.</summary>
|
||||
public const float DefaultBodyCollisionDistanceFactor = 0.75f;
|
||||
|
||||
/// <summary>Body collision distance factor for the relaxed fallback pass.</summary>
|
||||
public const float DefaultBodyCollisionNoOpDistanceFactor = 0.25f;
|
||||
|
||||
/// <summary>Relax multiplier applied when the mesh is close to the body.</summary>
|
||||
public const float DefaultBodyCollisionAdaptiveRelaxFactor = 1.0f;
|
||||
|
||||
/// <summary>Ratio of near-body vertices required to trigger relaxation.</summary>
|
||||
public const float DefaultBodyCollisionAdaptiveNearRatio = 0.4f;
|
||||
|
||||
/// <summary>UV threshold for relaxed body-collision mode.</summary>
|
||||
public const float DefaultBodyCollisionAdaptiveUvThreshold = 0.08f;
|
||||
|
||||
/// <summary>UV seam cosine threshold for relaxed body-collision mode.</summary>
|
||||
public const float DefaultBodyCollisionNoOpUvSeamAngleCos = 0.98f;
|
||||
|
||||
/// <summary>Expansion factor for protected vertices near the body.</summary>
|
||||
public const float DefaultBodyCollisionProtectionFactor = 1.5f;
|
||||
|
||||
/// <summary>Minimum ratio used when decimating the body proxy.</summary>
|
||||
public const float DefaultBodyProxyTargetRatioMin = 0.85f;
|
||||
|
||||
/// <summary>Inflation applied to body collision distances.</summary>
|
||||
public const float DefaultBodyCollisionProxyInflate = 0.0005f;
|
||||
|
||||
/// <summary>Body collision penetration factor used during collapse checks.</summary>
|
||||
public const float DefaultBodyCollisionPenetrationFactor = 0.75f;
|
||||
|
||||
/// <summary>Minimum body collision distance threshold.</summary>
|
||||
public const float DefaultMinBodyCollisionDistance = 0.0001f;
|
||||
|
||||
/// <summary>Minimum cell size for body collision spatial hashing.</summary>
|
||||
public const float DefaultMinBodyCollisionCellSize = 0.0001f;
|
||||
|
||||
/// <summary>Minimum triangles per connected component before skipping decimation.</summary>
|
||||
public int MinComponentTriangles { get; set; } = DefaultMinComponentTriangles;
|
||||
|
||||
/// <summary>Average-edge multiplier used to cap collapses.</summary>
|
||||
public float MaxCollapseEdgeLengthFactor { get; set; } = DefaultMaxCollapseEdgeLengthFactor;
|
||||
|
||||
/// <summary>Maximum normal deviation (degrees) allowed for a collapse.</summary>
|
||||
public float NormalSimilarityThresholdDegrees { get; set; } = DefaultNormalSimilarityThresholdDegrees;
|
||||
|
||||
/// <summary>Minimum bone-weight overlap required to allow a collapse.</summary>
|
||||
public float BoneWeightSimilarityThreshold { get; set; } = DefaultBoneWeightSimilarityThreshold;
|
||||
|
||||
/// <summary>UV similarity threshold to protect seams.</summary>
|
||||
public float UvSimilarityThreshold { get; set; } = DefaultUvSimilarityThreshold;
|
||||
|
||||
/// <summary>UV seam cosine threshold for blocking seam collapses.</summary>
|
||||
public float UvSeamAngleCos { get; set; } = DefaultUvSeamAngleCos;
|
||||
|
||||
/// <summary>Whether to block UV seam vertices from collapsing.</summary>
|
||||
public bool BlockUvSeamVertices { get; set; } = DefaultBlockUvSeamVertices;
|
||||
|
||||
/// <summary>Whether to allow collapses on boundary edges.</summary>
|
||||
public bool AllowBoundaryCollapses { get; set; } = DefaultAllowBoundaryCollapses;
|
||||
|
||||
/// <summary>Body collision distance factor for the primary pass.</summary>
|
||||
public float BodyCollisionDistanceFactor { get; set; } = DefaultBodyCollisionDistanceFactor;
|
||||
|
||||
/// <summary>Body collision distance factor for the relaxed fallback pass.</summary>
|
||||
public float BodyCollisionNoOpDistanceFactor { get; set; } = DefaultBodyCollisionNoOpDistanceFactor;
|
||||
|
||||
/// <summary>Relax multiplier applied when the mesh is close to the body.</summary>
|
||||
public float BodyCollisionAdaptiveRelaxFactor { get; set; } = DefaultBodyCollisionAdaptiveRelaxFactor;
|
||||
|
||||
/// <summary>Ratio of near-body vertices required to trigger relaxation.</summary>
|
||||
public float BodyCollisionAdaptiveNearRatio { get; set; } = DefaultBodyCollisionAdaptiveNearRatio;
|
||||
|
||||
/// <summary>UV threshold for relaxed body-collision mode.</summary>
|
||||
public float BodyCollisionAdaptiveUvThreshold { get; set; } = DefaultBodyCollisionAdaptiveUvThreshold;
|
||||
|
||||
/// <summary>UV seam cosine threshold for relaxed body-collision mode.</summary>
|
||||
public float BodyCollisionNoOpUvSeamAngleCos { get; set; } = DefaultBodyCollisionNoOpUvSeamAngleCos;
|
||||
|
||||
/// <summary>Expansion factor for protected vertices near the body.</summary>
|
||||
public float BodyCollisionProtectionFactor { get; set; } = DefaultBodyCollisionProtectionFactor;
|
||||
|
||||
/// <summary>Minimum ratio used when decimating the body proxy.</summary>
|
||||
public float BodyProxyTargetRatioMin { get; set; } = DefaultBodyProxyTargetRatioMin;
|
||||
|
||||
/// <summary>Inflation applied to body collision distances.</summary>
|
||||
public float BodyCollisionProxyInflate { get; set; } = DefaultBodyCollisionProxyInflate;
|
||||
|
||||
/// <summary>Body collision penetration factor used during collapse checks.</summary>
|
||||
public float BodyCollisionPenetrationFactor { get; set; } = DefaultBodyCollisionPenetrationFactor;
|
||||
|
||||
/// <summary>Minimum body collision distance threshold.</summary>
|
||||
public float MinBodyCollisionDistance { get; set; } = DefaultMinBodyCollisionDistance;
|
||||
|
||||
/// <summary>Minimum cell size for body collision spatial hashing.</summary>
|
||||
public float MinBodyCollisionCellSize { get; set; } = DefaultMinBodyCollisionCellSize;
|
||||
}
|
||||
@@ -21,16 +21,26 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
||||
public bool EnableIndexTextureDownscale { get; set; } = false;
|
||||
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
||||
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
||||
public bool EnableUncompressedTextureCompression { get; set; } = false;
|
||||
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; } = 20_000;
|
||||
public double ModelDecimationTargetRatio { get; set; } = 0.8;
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public sealed class TempCollectionConfig : ILightlessConfiguration
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
public sealed class OrphanableTempCollectionEntry
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTime RegisteredAtUtc { get; set; } = DateTime.MinValue;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public sealed class TempCollectionConfigService : ConfigurationServiceBase<TempCollectionConfig>
|
||||
{
|
||||
public const string ConfigName = "tempcollections.json";
|
||||
|
||||
public TempCollectionConfigService(string configDir) : base(configDir) { }
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>2.0.2.76</Version>
|
||||
<Version>2.0.2.80</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
@@ -85,6 +85,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||
<ProjectReference Include="..\LightlessCompactor\LightlessCompactor.csproj" />
|
||||
<ProjectReference Include="..\LightlessCompactorWorker\LightlessCompactorWorker.csproj" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
||||
@@ -108,5 +110,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Update="DalamudPackager" Version="14.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<CompactorWorkerFiles Include="..\LightlessCompactorWorker\bin\$(Configuration)\net10.0\*.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyCompactorWorker" AfterTargets="Build">
|
||||
<Copy SourceFiles="@(CompactorWorkerFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -19,6 +19,7 @@ public class FileDownloadManagerFactory
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly ModelDecimationService _modelDecimationService;
|
||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||
private readonly FileDownloadDeduplicator _downloadDeduplicator;
|
||||
|
||||
public FileDownloadManagerFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -29,7 +30,8 @@ public class FileDownloadManagerFactory
|
||||
LightlessConfigService configService,
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
ModelDecimationService modelDecimationService,
|
||||
TextureMetadataHelper textureMetadataHelper)
|
||||
TextureMetadataHelper textureMetadataHelper,
|
||||
FileDownloadDeduplicator downloadDeduplicator)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
@@ -40,6 +42,7 @@ public class FileDownloadManagerFactory
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_modelDecimationService = modelDecimationService;
|
||||
_textureMetadataHelper = textureMetadataHelper;
|
||||
_downloadDeduplicator = downloadDeduplicator;
|
||||
}
|
||||
|
||||
public FileDownloadManager Create()
|
||||
@@ -53,6 +56,7 @@ public class FileDownloadManagerFactory
|
||||
_configService,
|
||||
_textureDownscaleService,
|
||||
_modelDecimationService,
|
||||
_textureMetadataHelper);
|
||||
_textureMetadataHelper,
|
||||
_downloadDeduplicator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
@@ -9,11 +8,12 @@ using LightlessSync.PlayerData.Data;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
@@ -34,7 +34,7 @@ public class PlayerDataFactory
|
||||
private const int _maxTransientResolvedEntries = 1000;
|
||||
|
||||
// Character build caches
|
||||
private readonly ConcurrentDictionary<nint, Task<CharacterDataFragment>> _characterBuildInflight = new();
|
||||
private readonly TaskRegistry<nint> _characterBuildInflight = new();
|
||||
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
|
||||
|
||||
// Time out thresholds
|
||||
@@ -119,39 +119,48 @@ public class PlayerDataFactory
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly int _drawObjectOffset =
|
||||
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
|
||||
|
||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
||||
=> await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
nint basePtr = playerPointer;
|
||||
|
||||
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||
if (!LooksLikeUserPtr(basePtr))
|
||||
return true;
|
||||
|
||||
nint drawObjAddr = basePtr + _drawObjectOffset;
|
||||
|
||||
if (!TryReadIntPtr(drawObjAddr, out var drawObj))
|
||||
return true;
|
||||
|
||||
return drawObj == 0;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
private static bool LooksLikeUserPtr(nint p)
|
||||
{
|
||||
if (playerPointer == IntPtr.Zero)
|
||||
return true;
|
||||
if (p == 0) return false;
|
||||
|
||||
if (!IsPointerValid(playerPointer))
|
||||
return true;
|
||||
ulong u = (ulong)p;
|
||||
|
||||
var character = (Character*)playerPointer;
|
||||
if (character == null)
|
||||
return true;
|
||||
if (u < 0x0000_0001_0000UL) return false;
|
||||
if (u > 0x0000_7FFF_FFFF_FFFFUL) return false;
|
||||
if ((u & 0x7UL) != 0) return false;
|
||||
|
||||
var gameObject = &character->GameObject;
|
||||
if (gameObject == null)
|
||||
return true;
|
||||
|
||||
if (!IsPointerValid((IntPtr)gameObject))
|
||||
return true;
|
||||
|
||||
return gameObject->DrawObject == null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsPointerValid(IntPtr ptr)
|
||||
private static bool TryReadIntPtr(nint addr, out nint value)
|
||||
{
|
||||
if (ptr == IntPtr.Zero)
|
||||
value = 0;
|
||||
|
||||
if (!VirtualReadable(addr))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
_ = Marshal.ReadByte(ptr);
|
||||
value = Marshal.ReadIntPtr(addr);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
@@ -160,6 +169,37 @@ public class PlayerDataFactory
|
||||
}
|
||||
}
|
||||
|
||||
private static bool VirtualReadable(nint addr)
|
||||
{
|
||||
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
|
||||
return false;
|
||||
|
||||
const uint MEM_COMMIT = 0x1000;
|
||||
const uint PAGE_NOACCESS = 0x01;
|
||||
const uint PAGE_GUARD = 0x100;
|
||||
|
||||
if (mbi.State != MEM_COMMIT) return false;
|
||||
if ((mbi.Protect & PAGE_GUARD) != 0) return false;
|
||||
if (mbi.Protect == PAGE_NOACCESS) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nuint VirtualQuery(nint lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, nuint dwLength);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MEMORY_BASIC_INFORMATION
|
||||
{
|
||||
public nint BaseAddress;
|
||||
public nint AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public nuint RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
private static bool IsCacheFresh(CacheEntry entry)
|
||||
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
||||
|
||||
@@ -170,10 +210,10 @@ public class PlayerDataFactory
|
||||
{
|
||||
var key = obj.Address;
|
||||
|
||||
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
|
||||
if (_characterBuildCache.TryGetValue(key, out CacheEntry cached) && IsCacheFresh(cached) && !_characterBuildInflight.TryGetExisting(key, out _))
|
||||
return cached.Fragment;
|
||||
|
||||
var buildTask = _characterBuildInflight.GetOrAdd(key, _ => BuildAndCacheAsync(obj, key));
|
||||
Task<CharacterDataFragment> buildTask = _characterBuildInflight.GetOrStart(key, () => BuildAndCacheAsync(obj, key));
|
||||
|
||||
if (_characterBuildCache.TryGetValue(key, out cached))
|
||||
{
|
||||
@@ -189,20 +229,13 @@ public class PlayerDataFactory
|
||||
|
||||
private async Task<CharacterDataFragment> BuildAndCacheAsync(GameObjectHandler obj, nint key)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
||||
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
||||
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
||||
CharacterDataFragment fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
||||
|
||||
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
||||
PruneCharacterCacheIfNeeded();
|
||||
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
||||
PruneCharacterCacheIfNeeded();
|
||||
|
||||
return fragment;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_characterBuildInflight.TryRemove(key, out _);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private void PruneCharacterCacheIfNeeded()
|
||||
@@ -257,7 +290,28 @@ public class PlayerDataFactory
|
||||
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
|
||||
}
|
||||
|
||||
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false) ?? throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
|
||||
Guid penumbraRequestId = Guid.Empty;
|
||||
Stopwatch? penumbraSw = null;
|
||||
if (logDebug)
|
||||
{
|
||||
penumbraRequestId = Guid.NewGuid();
|
||||
penumbraSw = Stopwatch.StartNew();
|
||||
_logger.LogDebug("Penumbra GetCharacterData start {id} for {obj}", penumbraRequestId, playerRelatedObject);
|
||||
}
|
||||
|
||||
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false);
|
||||
|
||||
if (logDebug)
|
||||
{
|
||||
penumbraSw!.Stop();
|
||||
_logger.LogDebug("Penumbra GetCharacterData done {id} in {elapsedMs}ms (count={count})",
|
||||
penumbraRequestId,
|
||||
penumbraSw.ElapsedMilliseconds,
|
||||
resolvedPaths?.Count ?? -1);
|
||||
}
|
||||
|
||||
if (resolvedPaths == null)
|
||||
throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct);
|
||||
@@ -476,7 +530,7 @@ public class PlayerDataFactory
|
||||
if (transientPaths.Count == 0)
|
||||
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
|
||||
|
||||
var resolved = await GetFileReplacementsFromPaths(obj, transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
||||
var resolved = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
|
||||
@@ -678,7 +732,6 @@ public class PlayerDataFactory
|
||||
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
|
||||
GameObjectHandler handler,
|
||||
HashSet<string> forwardResolve,
|
||||
HashSet<string> reverseResolve)
|
||||
{
|
||||
@@ -693,59 +746,6 @@ public class PlayerDataFactory
|
||||
var reversePathsLower = reversePaths.Length == 0 ? [] : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
|
||||
|
||||
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
|
||||
if (handler.ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (!idx.HasValue)
|
||||
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
|
||||
|
||||
var resolvedForward = new string[forwardPaths.Length];
|
||||
for (int i = 0; i < forwardPaths.Length; i++)
|
||||
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
|
||||
|
||||
var resolvedReverse = new string[reversePaths.Length][];
|
||||
for (int i = 0; i < reversePaths.Length; i++)
|
||||
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
|
||||
|
||||
return (idx, resolvedForward, resolvedReverse);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (objectIndex.HasValue)
|
||||
{
|
||||
for (int i = 0; i < forwardPaths.Length; i++)
|
||||
{
|
||||
var filePath = forwardResolved[i]?.ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
continue;
|
||||
|
||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||
else
|
||||
{
|
||||
resolvedPaths[filePath] = [forwardPathsLower[i]];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < reversePaths.Length; i++)
|
||||
{
|
||||
var filePath = reversePathsLower[i];
|
||||
var reverseResolvedLower = new string[reverseResolved[i].Length];
|
||||
for (var j = 0; j < reverseResolvedLower.Length; j++)
|
||||
{
|
||||
reverseResolvedLower[j] = reverseResolved[i][j].ToLowerInvariant();
|
||||
}
|
||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
|
||||
else
|
||||
resolvedPaths[filePath] = [.. reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList()];
|
||||
}
|
||||
|
||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||
|
||||
for (int i = 0; i < forwardPaths.Length; i++)
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
bool PendingModReapply { get; }
|
||||
bool ModApplyDeferred { get; }
|
||||
int MissingCriticalMods { get; }
|
||||
int MissingNonCriticalMods { get; }
|
||||
int MissingForbiddenMods { get; }
|
||||
DateTime? InvisibleSinceUtc { get; }
|
||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
bool PendingModReapply { get; }
|
||||
bool ModApplyDeferred { get; }
|
||||
int MissingCriticalMods { get; }
|
||||
int MissingNonCriticalMods { get; }
|
||||
int MissingForbiddenMods { get; }
|
||||
|
||||
void Initialize();
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
Task EnsurePerformanceMetricsAsync(CancellationToken cancellationToken);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
|
||||
@@ -217,12 +217,6 @@ public class Pair
|
||||
if (handler is null)
|
||||
return PairDebugInfo.Empty;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
||||
var remainingSeconds = dueAt.HasValue
|
||||
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
||||
: (double?)null;
|
||||
|
||||
return new PairDebugInfo(
|
||||
true,
|
||||
handler.Initialized,
|
||||
@@ -231,9 +225,6 @@ public class Pair
|
||||
handler.LastDataReceivedAt,
|
||||
handler.LastApplyAttemptAt,
|
||||
handler.LastSuccessfulApplyAt,
|
||||
handler.InvisibleSinceUtc,
|
||||
handler.VisibilityEvictionDueAtUtc,
|
||||
remainingSeconds,
|
||||
handler.LastFailureReason,
|
||||
handler.LastBlockingConditions,
|
||||
handler.IsApplying,
|
||||
|
||||
@@ -137,7 +137,7 @@ public sealed partial class PairCoordinator
|
||||
_pendingCharacterData.TryRemove(user.UID, out _);
|
||||
if (registrationResult.Value.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value);
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value, forceDisposal: true);
|
||||
}
|
||||
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
||||
|
||||
@@ -8,9 +8,6 @@ public sealed record PairDebugInfo(
|
||||
DateTime? LastDataReceivedAt,
|
||||
DateTime? LastApplyAttemptAt,
|
||||
DateTime? LastSuccessfulApplyAt,
|
||||
DateTime? InvisibleSinceUtc,
|
||||
DateTime? VisibilityEvictionDueAtUtc,
|
||||
double? VisibilityEvictionRemainingSeconds,
|
||||
string? LastFailureReason,
|
||||
IReadOnlyList<string> BlockingConditions,
|
||||
bool IsApplying,
|
||||
@@ -32,9 +29,6 @@ public sealed record PairDebugInfo(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<string>(),
|
||||
false,
|
||||
false,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -136,6 +136,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
if (TryFinalizeHandlerRemoval(handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
_pairStateCache.Clear(registration.CharacterIdent);
|
||||
}
|
||||
}
|
||||
else if (shouldScheduleRemoval && handler is not null)
|
||||
@@ -356,6 +357,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
finally
|
||||
{
|
||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||
_pairStateCache.Clear(handler.Ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,6 +379,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
{
|
||||
handler.Dispose();
|
||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||
_pairStateCache.Clear(handler.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,6 +404,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
if (TryFinalizeHandlerRemoval(handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
_pairStateCache.Clear(handler.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,20 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
||||
|
||||
try
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced: true);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.EnsurePerformanceMetricsAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to ensure performance metrics for {Ident}", handler.Ident);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -160,8 +160,9 @@ public sealed class PairManager
|
||||
return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
|
||||
}
|
||||
|
||||
var ident = connection.Ident;
|
||||
connection.SetOffline();
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident));
|
||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), ident));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +531,7 @@ public sealed class PairManager
|
||||
return null;
|
||||
}
|
||||
|
||||
var ident = connection.Ident;
|
||||
if (connection.IsOnline)
|
||||
{
|
||||
connection.SetOffline();
|
||||
@@ -542,7 +544,7 @@ public sealed class PairManager
|
||||
shell.Users.Remove(userId);
|
||||
}
|
||||
|
||||
return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident);
|
||||
return new PairRegistration(new PairUniqueIdentifier(userId), ident);
|
||||
}
|
||||
|
||||
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
||||
|
||||
@@ -76,6 +76,7 @@ public sealed class PairConnection
|
||||
public void SetOffline()
|
||||
{
|
||||
IsOnline = false;
|
||||
Ident = null;
|
||||
}
|
||||
|
||||
public void UpdatePermissions(UserPermissions own, UserPermissions other)
|
||||
|
||||
@@ -129,12 +129,15 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton<TextureDownscaleService>();
|
||||
services.AddSingleton<ModelDecimationService>();
|
||||
services.AddSingleton<GameObjectHandlerFactory>();
|
||||
services.AddSingleton<FileDownloadDeduplicator>();
|
||||
services.AddSingleton<FileDownloadManagerFactory>();
|
||||
services.AddSingleton<PairProcessingLimiter>();
|
||||
services.AddSingleton<XivDataAnalyzer>();
|
||||
services.AddSingleton<CharacterAnalyzer>();
|
||||
services.AddSingleton<TokenProvider>();
|
||||
services.AddSingleton<PluginWarningNotificationService>();
|
||||
services.AddSingleton<ICompactorContext, PluginCompactorContext>();
|
||||
services.AddSingleton<ICompactionExecutor, ExternalCompactionExecutor>();
|
||||
services.AddSingleton<FileCompactor>();
|
||||
services.AddSingleton<TagHandler>();
|
||||
services.AddSingleton<PairRequestService>();
|
||||
@@ -331,8 +334,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<RedrawManager>(),
|
||||
sp.GetRequiredService<ActorObjectService>()));
|
||||
sp.GetRequiredService<RedrawManager>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerGlamourer(
|
||||
sp.GetRequiredService<ILogger<IpcCallerGlamourer>>(),
|
||||
@@ -427,6 +429,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||
return cfg;
|
||||
});
|
||||
services.AddSingleton(sp => new TempCollectionConfigService(configDir));
|
||||
services.AddSingleton(sp => new ServerConfigService(configDir));
|
||||
services.AddSingleton(sp => new NotesConfigService(configDir));
|
||||
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
||||
@@ -440,6 +443,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<TempCollectionConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
||||
@@ -516,6 +520,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
sp.GetRequiredService<ILogger<UiService>>(),
|
||||
pluginInterface.UiBuilder,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<WindowSystem>(),
|
||||
sp.GetServices<WindowMediatorSubscriberBase>(),
|
||||
sp.GetRequiredService<UiFactory>(),
|
||||
|
||||
@@ -93,6 +93,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
}
|
||||
RefreshTrackedActors(force: true);
|
||||
});
|
||||
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearTrackingState());
|
||||
}
|
||||
|
||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||
@@ -342,18 +343,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DisposeHooks();
|
||||
_activePlayers.Clear();
|
||||
_gposePlayers.Clear();
|
||||
_actorsByHash.Clear();
|
||||
_actorsByName.Clear();
|
||||
_pendingHashResolutions.Clear();
|
||||
ClearTrackingState();
|
||||
_mediator.UnsubscribeAll(this);
|
||||
lock (_playerRelatedHandlerLock)
|
||||
{
|
||||
_playerRelatedHandlers.Clear();
|
||||
}
|
||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -580,36 +571,19 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
if (localPlayerAddress == nint.Zero)
|
||||
return nint.Zero;
|
||||
|
||||
var playerObject = (GameObject*)localPlayerAddress;
|
||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
||||
if (ownerEntityId == 0)
|
||||
return nint.Zero;
|
||||
|
||||
if (candidateAddress != nint.Zero)
|
||||
{
|
||||
var candidate = (GameObject*)candidateAddress;
|
||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||
{
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
return candidateAddress;
|
||||
}
|
||||
}
|
||||
var playerObject = (GameObject*)localPlayerAddress;
|
||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
||||
if (candidateAddress == nint.Zero)
|
||||
return nint.Zero;
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
||||
continue;
|
||||
|
||||
if (obj.ObjectKind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
|
||||
continue;
|
||||
|
||||
var candidate = (GameObject*)obj.Address;
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
return obj.Address;
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
var candidate = (GameObject*)candidateAddress;
|
||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||
return candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion
|
||||
? candidateAddress
|
||||
: nint.Zero;
|
||||
}
|
||||
|
||||
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
||||
@@ -629,22 +603,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
||||
continue;
|
||||
|
||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
||||
continue;
|
||||
|
||||
var candidate = (GameObject*)obj.Address;
|
||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
||||
continue;
|
||||
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
return obj.Address;
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
@@ -664,23 +622,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
||||
continue;
|
||||
|
||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
||||
continue;
|
||||
|
||||
var candidate = (GameObject*)obj.Address;
|
||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
|
||||
continue;
|
||||
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
return obj.Address;
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
@@ -1077,6 +1018,22 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTrackingState()
|
||||
{
|
||||
_activePlayers.Clear();
|
||||
_gposePlayers.Clear();
|
||||
_actorsByHash.Clear();
|
||||
_actorsByName.Clear();
|
||||
_pendingHashResolutions.Clear();
|
||||
lock (_playerRelatedHandlerLock)
|
||||
{
|
||||
_playerRelatedHandlers.Clear();
|
||||
}
|
||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||
_nextRefreshAllowed = DateTime.MinValue;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeHooks();
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
_baseAnalysisCts.Dispose();
|
||||
}
|
||||
|
||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token, bool force = false)
|
||||
{
|
||||
var normalized = new HashSet<string>(
|
||||
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<string>? _format;
|
||||
|
||||
public void RefreshFormat() => _format = CreateFormatValue();
|
||||
public void UpdateTriangles(long triangles) => Triangles = triangles;
|
||||
|
||||
private Lazy<string> CreateFormatValue()
|
||||
=> new(() =>
|
||||
|
||||
@@ -8,18 +8,26 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LightlessSync.Services.Chat;
|
||||
|
||||
public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private const int MaxMessageHistory = 150;
|
||||
private const int MaxMessageHistory = 200;
|
||||
internal const int MaxOutgoingLength = 200;
|
||||
private const int MaxUnreadCount = 999;
|
||||
private const string ZoneUnavailableMessage = "Zone chat is only available in major cities.";
|
||||
private const string ZoneChannelKey = "zone";
|
||||
private const int MaxReportReasonLength = 100;
|
||||
private const int MaxReportContextLength = 1000;
|
||||
private static readonly JsonSerializerOptions PersistedHistorySerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ApiController _apiController;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
@@ -376,6 +384,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
LoadPersistedSyncshellHistory();
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => HandleLogin());
|
||||
Mediator.Subscribe<DalamudLogoutMessage>(this, _ => HandleLogout());
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ScheduleZonePresenceUpdate());
|
||||
@@ -1000,11 +1009,22 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
private void OnChatMessageReceived(ChatMessageDto dto)
|
||||
{
|
||||
var descriptor = dto.Channel.WithNormalizedCustomKey();
|
||||
var key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
||||
var fromSelf = IsMessageFromSelf(dto, key);
|
||||
var message = BuildMessage(dto, fromSelf);
|
||||
ChatChannelDescriptor descriptor = dto.Channel.WithNormalizedCustomKey();
|
||||
string key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
||||
bool fromSelf = IsMessageFromSelf(dto, key);
|
||||
ChatMessageEntry message = BuildMessage(dto, fromSelf);
|
||||
bool mentionNotificationsEnabled = _chatConfigService.Current.EnableMentionNotifications;
|
||||
bool notifyMention = mentionNotificationsEnabled
|
||||
&& !fromSelf
|
||||
&& descriptor.Type == ChatChannelType.Group
|
||||
&& TryGetSelfMentionToken(dto.Message, out _);
|
||||
|
||||
string? mentionChannelName = null;
|
||||
string? mentionSenderName = null;
|
||||
bool publishChannelList = false;
|
||||
bool shouldPersistHistory = _chatConfigService.Current.PersistSyncshellHistory;
|
||||
List<PersistedChatMessage>? persistedMessages = null;
|
||||
string? persistedChannelKey = null;
|
||||
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
@@ -1042,6 +1062,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.Messages.RemoveAt(0);
|
||||
}
|
||||
|
||||
if (notifyMention)
|
||||
{
|
||||
mentionChannelName = state.DisplayName;
|
||||
mentionSenderName = message.DisplayName;
|
||||
}
|
||||
|
||||
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
||||
{
|
||||
state.HasUnread = false;
|
||||
@@ -1058,10 +1084,29 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
|
||||
if (shouldPersistHistory && state.Type == ChatChannelType.Group)
|
||||
{
|
||||
persistedChannelKey = state.Key;
|
||||
persistedMessages = BuildPersistedHistoryLocked(state);
|
||||
}
|
||||
}
|
||||
|
||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||
|
||||
if (persistedMessages is not null && persistedChannelKey is not null)
|
||||
{
|
||||
PersistSyncshellHistory(persistedChannelKey, persistedMessages);
|
||||
}
|
||||
|
||||
if (notifyMention)
|
||||
{
|
||||
string channelName = mentionChannelName ?? "Syncshell";
|
||||
string senderName = mentionSenderName ?? "Someone";
|
||||
string notificationText = $"You were mentioned by {senderName} in {channelName}.";
|
||||
Mediator.Publish(new NotificationMessage("Syncshell mention", notificationText, NotificationType.Info));
|
||||
}
|
||||
|
||||
if (publishChannelList)
|
||||
{
|
||||
using (_sync.EnterScope())
|
||||
@@ -1108,6 +1153,113 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetSelfMentionToken(string message, out string matchedToken)
|
||||
{
|
||||
matchedToken = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HashSet<string> tokens = BuildSelfMentionTokens();
|
||||
if (tokens.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryFindMentionToken(message, tokens, out matchedToken);
|
||||
}
|
||||
|
||||
private HashSet<string> BuildSelfMentionTokens()
|
||||
{
|
||||
HashSet<string> tokens = new(StringComparer.OrdinalIgnoreCase);
|
||||
string uid = _apiController.UID;
|
||||
if (IsValidMentionToken(uid))
|
||||
{
|
||||
tokens.Add(uid);
|
||||
}
|
||||
|
||||
string displayName = _apiController.DisplayName;
|
||||
if (IsValidMentionToken(displayName))
|
||||
{
|
||||
tokens.Add(displayName);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static bool IsValidMentionToken(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
if (!IsMentionChar(value[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFindMentionToken(string message, IReadOnlyCollection<string> tokens, out string matchedToken)
|
||||
{
|
||||
matchedToken = string.Empty;
|
||||
if (tokens.Count == 0 || string.IsNullOrEmpty(message))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
while (index < message.Length)
|
||||
{
|
||||
if (message[index] != '@')
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index > 0 && IsMentionChar(message[index - 1]))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int start = index + 1;
|
||||
int end = start;
|
||||
while (end < message.Length && IsMentionChar(message[end]))
|
||||
{
|
||||
end++;
|
||||
}
|
||||
|
||||
if (end == start)
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
string token = message.Substring(start, end - start);
|
||||
if (tokens.Contains(token))
|
||||
{
|
||||
matchedToken = token;
|
||||
return true;
|
||||
}
|
||||
|
||||
index = end;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsMentionChar(char value)
|
||||
{
|
||||
return char.IsLetterOrDigit(value) || value == '_' || value == '-' || value == '\'';
|
||||
}
|
||||
|
||||
private ChatMessageEntry BuildMessage(ChatMessageDto dto, bool fromSelf)
|
||||
{
|
||||
var displayName = ResolveDisplayName(dto, fromSelf);
|
||||
@@ -1364,6 +1516,313 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void LoadPersistedSyncshellHistory()
|
||||
{
|
||||
if (!_chatConfigService.Current.PersistSyncshellHistory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
||||
if (persisted.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> invalidKeys = new();
|
||||
foreach (KeyValuePair<string, string> entry in persisted)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.Key) || string.IsNullOrWhiteSpace(entry.Value))
|
||||
{
|
||||
invalidKeys.Add(entry.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryDecodePersistedHistory(entry.Value, out List<PersistedChatMessage> persistedMessages))
|
||||
{
|
||||
invalidKeys.Add(entry.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistedMessages.Count == 0)
|
||||
{
|
||||
invalidKeys.Add(entry.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistedMessages.Count > MaxMessageHistory)
|
||||
{
|
||||
int startIndex = Math.Max(0, persistedMessages.Count - MaxMessageHistory);
|
||||
persistedMessages = persistedMessages.GetRange(startIndex, persistedMessages.Count - startIndex);
|
||||
}
|
||||
|
||||
List<ChatMessageEntry> restoredMessages = new(persistedMessages.Count);
|
||||
foreach (PersistedChatMessage persistedMessage in persistedMessages)
|
||||
{
|
||||
if (!TryBuildRestoredMessage(entry.Key, persistedMessage, out ChatMessageEntry restoredMessage))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
restoredMessages.Add(restoredMessage);
|
||||
}
|
||||
|
||||
if (restoredMessages.Count == 0)
|
||||
{
|
||||
invalidKeys.Add(entry.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_messageHistoryCache[entry.Key] = restoredMessages;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidKeys.Count > 0)
|
||||
{
|
||||
foreach (string key in invalidKeys)
|
||||
{
|
||||
persisted.Remove(key);
|
||||
}
|
||||
|
||||
_chatConfigService.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private List<PersistedChatMessage> BuildPersistedHistoryLocked(ChatChannelState state)
|
||||
{
|
||||
int startIndex = Math.Max(0, state.Messages.Count - MaxMessageHistory);
|
||||
List<PersistedChatMessage> persistedMessages = new(state.Messages.Count - startIndex);
|
||||
for (int i = startIndex; i < state.Messages.Count; i++)
|
||||
{
|
||||
ChatMessageEntry entry = state.Messages[i];
|
||||
if (entry.Payload is not { } payload)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
persistedMessages.Add(new PersistedChatMessage(
|
||||
payload.Message,
|
||||
entry.DisplayName,
|
||||
entry.FromSelf,
|
||||
entry.ReceivedAtUtc,
|
||||
payload.SentAtUtc));
|
||||
}
|
||||
|
||||
return persistedMessages;
|
||||
}
|
||||
|
||||
private void PersistSyncshellHistory(string channelKey, List<PersistedChatMessage> persistedMessages)
|
||||
{
|
||||
if (!_chatConfigService.Current.PersistSyncshellHistory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
||||
if (persistedMessages.Count == 0)
|
||||
{
|
||||
if (persisted.Remove(channelKey))
|
||||
{
|
||||
_chatConfigService.Save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string? base64 = EncodePersistedMessages(persistedMessages);
|
||||
if (string.IsNullOrWhiteSpace(base64))
|
||||
{
|
||||
if (persisted.Remove(channelKey))
|
||||
{
|
||||
_chatConfigService.Save();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
persisted[channelKey] = base64;
|
||||
_chatConfigService.Save();
|
||||
}
|
||||
|
||||
private static string? EncodePersistedMessages(List<PersistedChatMessage> persistedMessages)
|
||||
{
|
||||
if (persistedMessages.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(persistedMessages, PersistedHistorySerializerOptions);
|
||||
return Convert.ToBase64String(jsonBytes);
|
||||
}
|
||||
|
||||
private static bool TryDecodePersistedHistory(string base64, out List<PersistedChatMessage> persistedMessages)
|
||||
{
|
||||
persistedMessages = new List<PersistedChatMessage>();
|
||||
if (string.IsNullOrWhiteSpace(base64))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] jsonBytes = Convert.FromBase64String(base64);
|
||||
List<PersistedChatMessage>? decoded = JsonSerializer.Deserialize<List<PersistedChatMessage>>(jsonBytes, PersistedHistorySerializerOptions);
|
||||
if (decoded is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
persistedMessages = decoded;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryBuildRestoredMessage(string channelKey, PersistedChatMessage persistedMessage, out ChatMessageEntry restoredMessage)
|
||||
{
|
||||
restoredMessage = default;
|
||||
string messageText = persistedMessage.Message;
|
||||
DateTime sentAtUtc = persistedMessage.SentAtUtc;
|
||||
if (string.IsNullOrWhiteSpace(messageText) && persistedMessage.LegacyPayload is { } legacy)
|
||||
{
|
||||
messageText = legacy.Message;
|
||||
sentAtUtc = legacy.SentAtUtc;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(messageText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ChatChannelDescriptor descriptor = BuildDescriptorFromChannelKey(channelKey);
|
||||
ChatSenderDescriptor sender = new ChatSenderDescriptor(
|
||||
ChatSenderKind.Anonymous,
|
||||
string.Empty,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false);
|
||||
|
||||
ChatMessageDto payload = new ChatMessageDto(descriptor, sender, messageText, sentAtUtc, string.Empty);
|
||||
restoredMessage = new ChatMessageEntry(payload, persistedMessage.DisplayName, persistedMessage.FromSelf, persistedMessage.ReceivedAtUtc);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ChatChannelDescriptor BuildDescriptorFromChannelKey(string channelKey)
|
||||
{
|
||||
if (string.Equals(channelKey, ZoneChannelKey, StringComparison.Ordinal))
|
||||
{
|
||||
return new ChatChannelDescriptor { Type = ChatChannelType.Zone };
|
||||
}
|
||||
|
||||
int separatorIndex = channelKey.IndexOf(':', StringComparison.Ordinal);
|
||||
if (separatorIndex <= 0 || separatorIndex >= channelKey.Length - 1)
|
||||
{
|
||||
return new ChatChannelDescriptor { Type = ChatChannelType.Group };
|
||||
}
|
||||
|
||||
string typeValue = channelKey[..separatorIndex];
|
||||
if (!int.TryParse(typeValue, out int parsedType))
|
||||
{
|
||||
return new ChatChannelDescriptor { Type = ChatChannelType.Group };
|
||||
}
|
||||
|
||||
string customKey = channelKey[(separatorIndex + 1)..];
|
||||
ChatChannelType channelType = parsedType switch
|
||||
{
|
||||
(int)ChatChannelType.Zone => ChatChannelType.Zone,
|
||||
(int)ChatChannelType.Group => ChatChannelType.Group,
|
||||
_ => ChatChannelType.Group
|
||||
};
|
||||
|
||||
return new ChatChannelDescriptor
|
||||
{
|
||||
Type = channelType,
|
||||
CustomKey = customKey
|
||||
};
|
||||
}
|
||||
|
||||
public void ClearPersistedSyncshellHistory(bool clearLoadedMessages)
|
||||
{
|
||||
bool shouldPublish = false;
|
||||
bool saveConfig = false;
|
||||
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
Dictionary<string, List<ChatMessageEntry>> cache = _messageHistoryCache;
|
||||
if (cache.Count > 0)
|
||||
{
|
||||
List<string> keysToRemove = new();
|
||||
foreach (string key in cache.Keys)
|
||||
{
|
||||
if (!string.Equals(key, ZoneChannelKey, StringComparison.Ordinal))
|
||||
{
|
||||
keysToRemove.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string key in keysToRemove)
|
||||
{
|
||||
cache.Remove(key);
|
||||
}
|
||||
|
||||
if (keysToRemove.Count > 0)
|
||||
{
|
||||
shouldPublish = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (clearLoadedMessages)
|
||||
{
|
||||
foreach (ChatChannelState state in _channels.Values)
|
||||
{
|
||||
if (state.Type != ChatChannelType.Group)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state.Messages.Count == 0 && state.UnreadCount == 0 && !state.HasUnread)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
state.Messages.Clear();
|
||||
state.HasUnread = false;
|
||||
state.UnreadCount = 0;
|
||||
_lastReadCounts[state.Key] = 0;
|
||||
shouldPublish = true;
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
||||
if (persisted.Count > 0)
|
||||
{
|
||||
persisted.Clear();
|
||||
saveConfig = true;
|
||||
}
|
||||
|
||||
if (shouldPublish)
|
||||
{
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
if (saveConfig)
|
||||
{
|
||||
_chatConfigService.Save();
|
||||
}
|
||||
|
||||
if (shouldPublish)
|
||||
{
|
||||
PublishChannelListChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ChatChannelState
|
||||
{
|
||||
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)
|
||||
@@ -1400,4 +1859,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
bool IsOwner);
|
||||
|
||||
private readonly record struct PendingSelfMessage(string ChannelKey, string Message);
|
||||
|
||||
public sealed record PersistedChatMessage(
|
||||
string Message = "",
|
||||
string DisplayName = "",
|
||||
bool FromSelf = false,
|
||||
DateTime ReceivedAtUtc = default,
|
||||
DateTime SentAtUtc = default,
|
||||
[property: JsonPropertyName("Payload")] ChatMessageDto? LegacyPayload = null);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ using LightlessSync.Utils;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
@@ -93,7 +91,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
|
||||
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])
|
||||
|| w is { RowId: > 1000, Region: 101 or 201 }))
|
||||
|| w is { RowId: > 1000, UserType: 101 or 201 }))
|
||||
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
||||
});
|
||||
JobData = new(() =>
|
||||
@@ -229,6 +227,28 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_ = RunOnFrameworkThread(ReleaseFocusUnsafe);
|
||||
}
|
||||
|
||||
public void TargetPlayerByAddress(nint address)
|
||||
{
|
||||
if (address == nint.Zero) return;
|
||||
if (_clientState.IsPvP) return;
|
||||
|
||||
_ = RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObject = CreateGameObject(address);
|
||||
if (gameObject is null) return;
|
||||
|
||||
var useFocusTarget = _configService.Current.UseFocusTarget;
|
||||
if (useFocusTarget)
|
||||
{
|
||||
_targetManager.FocusTarget = gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
_targetManager.Target = gameObject;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent)
|
||||
{
|
||||
var target = CreateGameObject(address);
|
||||
@@ -404,38 +424,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
|
||||
|
||||
var playerAddress = playerPointer.Value;
|
||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
if (ownerEntityId == 0) return candidateAddress;
|
||||
|
||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
||||
{
|
||||
var localOwned = _actorObjectService.LocalMinionOrMountAddress;
|
||||
if (localOwned != nint.Zero)
|
||||
{
|
||||
return localOwned;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateAddress != nint.Zero)
|
||||
{
|
||||
var candidate = (GameObject*)candidateAddress;
|
||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
|
||||
&& ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return candidateAddress;
|
||||
}
|
||||
}
|
||||
|
||||
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||
if (ownedObject != nint.Zero)
|
||||
{
|
||||
return ownedObject;
|
||||
}
|
||||
|
||||
return candidateAddress;
|
||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||
@@ -465,7 +454,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
return FindOwnedPet(ownerEntityId, ownerAddress);
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
||||
@@ -473,69 +462,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private unsafe nint FindOwnedObject(uint ownerEntityId, nint ownerAddress, Func<DalamudObjectKind, bool> matchesKind)
|
||||
{
|
||||
if (ownerEntityId == 0)
|
||||
{
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!matchesKind(obj.ObjectKind))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = (GameObject*)obj.Address;
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return obj.Address;
|
||||
}
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
private unsafe nint FindOwnedPet(uint ownerEntityId, nint ownerAddress)
|
||||
{
|
||||
if (ownerEntityId == 0)
|
||||
{
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = (GameObject*)obj.Address;
|
||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return obj.Address;
|
||||
}
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
|
||||
{
|
||||
if (candidate == null)
|
||||
@@ -634,6 +560,37 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetHashedCIDFromAddress(nint address, out string hashedCid)
|
||||
{
|
||||
hashedCid = string.Empty;
|
||||
if (address == nint.Zero)
|
||||
return false;
|
||||
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
{
|
||||
return TryGetHashedCIDFromAddressInternal(address, out hashedCid);
|
||||
}
|
||||
|
||||
var result = _framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var success = TryGetHashedCIDFromAddressInternal(address, out var resolved);
|
||||
return (success, resolved);
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
hashedCid = result.resolved;
|
||||
return result.success;
|
||||
}
|
||||
|
||||
private bool TryGetHashedCIDFromAddressInternal(nint address, out string hashedCid)
|
||||
{
|
||||
hashedCid = string.Empty;
|
||||
var player = _objectTable.CreateObjectReference(address) as IPlayerCharacter;
|
||||
if (player == null || player.Address != address)
|
||||
return false;
|
||||
|
||||
return TryGetHashedCID(player, out hashedCid);
|
||||
}
|
||||
|
||||
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||
{
|
||||
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
||||
@@ -744,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
str += $" Room #{location.RoomId}";
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public string LocationToLifestream(LocationInfo location)
|
||||
{
|
||||
if (location.ServerId is 0 || location.TerritoryId is 0 || ContentFinderData.Value.ContainsKey(location.TerritoryId)) return String.Empty;
|
||||
var str = WorldData.Value[(ushort)location.ServerId];
|
||||
if (location.HouseId is 0 && location.MapId is not 0)
|
||||
{
|
||||
var mapName = MapData.Value[(ushort)location.MapId].MapName;
|
||||
var parts = mapName.Split(" - ", StringSplitOptions.RemoveEmptyEntries);
|
||||
var locationName = parts.Length > 0 ? parts[^1] : mapName;
|
||||
str += $", tp {locationName}";
|
||||
string message = $"LocationToLifestream: {str}";
|
||||
_logger.LogInformation(message);
|
||||
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
@@ -816,9 +789,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
_logger.LogInformation("Starting DalamudUtilService");
|
||||
_framework.Update += FrameworkOnUpdate;
|
||||
if (IsLoggedIn)
|
||||
_clientState.Login += OnClientLogin;
|
||||
_clientState.Logout += OnClientLogout;
|
||||
|
||||
if (_clientState.IsLoggedIn)
|
||||
{
|
||||
_classJobId = _objectTable.LocalPlayer!.ClassJob.RowId;
|
||||
OnClientLogin();
|
||||
}
|
||||
|
||||
_logger.LogInformation("Started DalamudUtilService");
|
||||
@@ -831,6 +807,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
Mediator.UnsubscribeAll(this);
|
||||
_framework.Update -= FrameworkOnUpdate;
|
||||
_clientState.Login -= OnClientLogin;
|
||||
_clientState.Logout -= OnClientLogout;
|
||||
if (_FocusPairIdent.HasValue)
|
||||
{
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
@@ -845,43 +823,72 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task WaitWhileCharacterIsDrawing(
|
||||
ILogger logger,
|
||||
GameObjectHandler handler,
|
||||
Guid redrawId,
|
||||
int timeOut = 5000,
|
||||
CancellationToken? ct = null)
|
||||
private void OnClientLogin()
|
||||
{
|
||||
if (IsLoggedIn)
|
||||
return;
|
||||
|
||||
_ = RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (IsLoggedIn)
|
||||
return;
|
||||
|
||||
var localPlayer = _objectTable.LocalPlayer;
|
||||
IsLoggedIn = true;
|
||||
_lastZone = _clientState.TerritoryType;
|
||||
if (localPlayer != null)
|
||||
{
|
||||
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
||||
_classJobId = localPlayer.ClassJob.RowId;
|
||||
}
|
||||
|
||||
_cid = RebuildCID();
|
||||
Mediator.Publish(new DalamudLoginMessage());
|
||||
});
|
||||
}
|
||||
|
||||
private void OnClientLogout(int type, int code)
|
||||
{
|
||||
if (!IsLoggedIn)
|
||||
return;
|
||||
_ = RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (!IsLoggedIn)
|
||||
return;
|
||||
|
||||
IsLoggedIn = false;
|
||||
_lastWorldId = 0;
|
||||
Mediator.Publish(new DalamudLogoutMessage());
|
||||
});
|
||||
}
|
||||
|
||||
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
|
||||
{
|
||||
if (!_clientState.IsLoggedIn) return;
|
||||
|
||||
var token = ct ?? CancellationToken.None;
|
||||
if (ct == null)
|
||||
ct = CancellationToken.None;
|
||||
|
||||
const int tick = 250;
|
||||
const int initialSettle = 50;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
int curWaitTime = 0;
|
||||
try
|
||||
{
|
||||
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
|
||||
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
||||
curWaitTime += tick;
|
||||
|
||||
await Task.Delay(initialSettle, token).ConfigureAwait(false);
|
||||
|
||||
while (!token.IsCancellationRequested
|
||||
&& sw.ElapsedMilliseconds < timeOut
|
||||
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false))
|
||||
while ((!ct.Value.IsCancellationRequested)
|
||||
&& curWaitTime < timeOut
|
||||
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
|
||||
{
|
||||
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
|
||||
await Task.Delay(tick, token).ConfigureAwait(false);
|
||||
curWaitTime += tick;
|
||||
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
logger.LogTrace("[{redrawId}] Finished drawing after {ms}ms", redrawId, sw.ElapsedMilliseconds);
|
||||
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
||||
}
|
||||
@@ -931,109 +938,37 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
||||
}
|
||||
|
||||
public void TargetPlayerByAddress(nint address)
|
||||
{
|
||||
if (address == nint.Zero) return;
|
||||
if (_clientState.IsPvP) return;
|
||||
|
||||
_ = RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObject = CreateGameObject(address);
|
||||
if (gameObject is null) return;
|
||||
|
||||
var useFocusTarget = _configService.Current.UseFocusTarget;
|
||||
if (useFocusTarget)
|
||||
{
|
||||
_targetManager.FocusTarget = gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
_targetManager.Target = gameObject;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool IsBadReadPtr(IntPtr ptr, UIntPtr size);
|
||||
|
||||
private static bool IsValidPointer(nint ptr, int size = 8)
|
||||
{
|
||||
if (ptr == nint.Zero)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!Util.IsWine())
|
||||
{
|
||||
return !IsBadReadPtr(ptr, (UIntPtr)size);
|
||||
}
|
||||
return ptr != nint.Zero && (ptr % IntPtr.Size) == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (!IsValidPointer(address))
|
||||
{
|
||||
_logger.LogDebug("Invalid pointer for character {name} at {addr}", characterName, address.ToString("X"));
|
||||
return;
|
||||
}
|
||||
|
||||
var gameObj = (GameObject*)address;
|
||||
|
||||
if (gameObj == null)
|
||||
return;
|
||||
|
||||
if (!_objectTable.Any(o => o?.Address == address))
|
||||
{
|
||||
_logger.LogDebug("Character {name} at {addr} no longer in object table", characterName, address.ToString("X"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameObj->ObjectKind == 0)
|
||||
return;
|
||||
|
||||
var drawObj = gameObj->DrawObject;
|
||||
bool isDrawing = false;
|
||||
bool isDrawingChanged = false;
|
||||
|
||||
if ((nint)drawObj != IntPtr.Zero && IsValidPointer((nint)drawObj))
|
||||
if ((nint)drawObj != IntPtr.Zero)
|
||||
{
|
||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||
|
||||
if (!isDrawing)
|
||||
{
|
||||
var charBase = (CharacterBase*)drawObj;
|
||||
if (charBase != null && IsValidPointer((nint)charBase))
|
||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||
if (!isDrawing)
|
||||
{
|
||||
isDrawing = charBase->HasModelInSlotLoaded != 0;
|
||||
if (!isDrawing)
|
||||
isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
|
||||
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
||||
{
|
||||
isDrawing = charBase->HasModelFilesInSlotLoaded != 0;
|
||||
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
||||
{
|
||||
_lastGlobalBlockPlayer = characterName;
|
||||
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
|
||||
isDrawingChanged = true;
|
||||
}
|
||||
_lastGlobalBlockPlayer = characterName;
|
||||
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
|
||||
isDrawingChanged = true;
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
|
||||
{
|
||||
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
|
||||
{
|
||||
_lastGlobalBlockPlayer = characterName;
|
||||
_lastGlobalBlockReason = "HasModelInSlotLoaded";
|
||||
isDrawingChanged = true;
|
||||
}
|
||||
_lastGlobalBlockPlayer = characterName;
|
||||
_lastGlobalBlockReason = "HasModelInSlotLoaded";
|
||||
isDrawingChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1064,21 +999,39 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
private unsafe void FrameworkOnUpdateInternal()
|
||||
{
|
||||
if (!_clientState.IsLoggedIn || _objectTable.LocalPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||
var localPlayer = _objectTable.LocalPlayer;
|
||||
if ((localPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddSeconds(1);
|
||||
var clientLoggedIn = _clientState.IsLoggedIn;
|
||||
|
||||
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
|
||||
{
|
||||
IsAnythingDrawing = false;
|
||||
|
||||
if (!isNormalFrameworkUpdate)
|
||||
{
|
||||
if (_gameConfig != null
|
||||
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
||||
{
|
||||
IsLodEnabled = lodEnabled;
|
||||
}
|
||||
|
||||
if (IsInCombat || IsPerforming || IsInInstance)
|
||||
Mediator.Publish(new FrameworkUpdateMessage());
|
||||
|
||||
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
||||
|
||||
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (!clientLoggedIn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||
() =>
|
||||
{
|
||||
@@ -1087,40 +1040,46 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
}
|
||||
|
||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||
var descriptorCount = playerDescriptors.Count;
|
||||
|
||||
for (var i = 0; i < descriptorCount; i++)
|
||||
if (_clientState.IsLoggedIn && localPlayer != null)
|
||||
{
|
||||
if (i >= playerDescriptors.Count)
|
||||
break;
|
||||
|
||||
var actor = playerDescriptors[i];
|
||||
|
||||
var playerAddress = actor.Address;
|
||||
if (playerAddress == nint.Zero || !IsValidPointer(playerAddress))
|
||||
continue;
|
||||
|
||||
if (actor.ObjectIndex >= 200)
|
||||
continue;
|
||||
|
||||
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, actor.ObjectIndex, out bool firstTime) && firstTime)
|
||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||
{
|
||||
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
|
||||
continue;
|
||||
}
|
||||
var actor = playerDescriptors[i];
|
||||
|
||||
if (!IsAnythingDrawing)
|
||||
{
|
||||
if (!_objectTable.Any(o => o?.Address == playerAddress))
|
||||
var playerAddress = actor.Address;
|
||||
if (playerAddress == nint.Zero)
|
||||
continue;
|
||||
|
||||
if (actor.ObjectIndex >= 200)
|
||||
continue;
|
||||
|
||||
var obj = _objectTable[actor.ObjectIndex];
|
||||
if (obj is not IPlayerCharacter player || player.Address != playerAddress)
|
||||
continue;
|
||||
|
||||
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, actor.ObjectIndex, out bool firstTime) && firstTime)
|
||||
{
|
||||
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
|
||||
continue;
|
||||
}
|
||||
|
||||
CheckCharacterForDrawing(playerAddress, actor.Name);
|
||||
if (!IsAnythingDrawing)
|
||||
{
|
||||
var charaName = player.Name.TextValue;
|
||||
if (string.IsNullOrEmpty(charaName))
|
||||
{
|
||||
charaName = actor.Name;
|
||||
}
|
||||
|
||||
if (IsAnythingDrawing)
|
||||
CheckCharacterForDrawing(playerAddress, charaName);
|
||||
if (IsAnythingDrawing)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1246,7 +1205,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
}
|
||||
|
||||
|
||||
var localPlayer = _objectTable.LocalPlayer;
|
||||
if (localPlayer != null)
|
||||
{
|
||||
_classJobId = localPlayer.ClassJob.RowId;
|
||||
@@ -1268,39 +1226,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
Mediator.Publish(new FrameworkUpdateMessage());
|
||||
|
||||
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
||||
|
||||
if (isNormalFrameworkUpdate)
|
||||
return;
|
||||
|
||||
if (localPlayer != null && !IsLoggedIn)
|
||||
{
|
||||
_logger.LogDebug("Logged in");
|
||||
IsLoggedIn = true;
|
||||
_lastZone = _clientState.TerritoryType;
|
||||
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
||||
_cid = RebuildCID();
|
||||
Mediator.Publish(new DalamudLoginMessage());
|
||||
}
|
||||
else if (localPlayer == null && IsLoggedIn)
|
||||
{
|
||||
_logger.LogDebug("Logged out");
|
||||
IsLoggedIn = false;
|
||||
_lastWorldId = 0;
|
||||
Mediator.Publish(new DalamudLogoutMessage());
|
||||
}
|
||||
|
||||
if (_gameConfig != null
|
||||
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
||||
{
|
||||
IsLodEnabled = lodEnabled;
|
||||
}
|
||||
|
||||
if (IsInCombat || IsPerforming || IsInInstance)
|
||||
Mediator.Publish(new FrameworkUpdateMessage());
|
||||
|
||||
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
||||
|
||||
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1330,4 +1255,4 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
onExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Lifestream.Enums;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.User;
|
||||
@@ -108,6 +109,144 @@ namespace LightlessSync.Services
|
||||
}
|
||||
}
|
||||
|
||||
public LocationInfo? GetLocationForLifestreamByUid(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
|
||||
{
|
||||
return location;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e,"GetLocationInfoByUid error : ");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public AddressBookEntryTuple? GetAddressBookEntryByLocation(LocationInfo location)
|
||||
{
|
||||
if (location.ServerId is 0 || location.TerritoryId is 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
||||
|
||||
if (territoryHousing == TerritoryTypeIdHousing.None || !Enum.IsDefined(typeof(TerritoryTypeIdHousing), territoryHousing))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var city = GetResidentialAetheryteKind(territoryHousing);
|
||||
|
||||
if (city == ResidentialAetheryteKind.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (location.HouseId is not 0 and not 100)
|
||||
{
|
||||
AddressBookEntryTuple addressEntry = (
|
||||
Name: "",
|
||||
World: (int)location.ServerId,
|
||||
City: (int)city,
|
||||
Ward: (int)location.WardId,
|
||||
PropertyType: 0,
|
||||
Plot: (int)location.HouseId,
|
||||
Apartment: 0,
|
||||
ApartmentSubdivision: location.DivisionId == 2,
|
||||
AliasEnabled: false,
|
||||
Alias: ""
|
||||
);
|
||||
return addressEntry;
|
||||
}
|
||||
else if (location.HouseId is 100)
|
||||
{
|
||||
AddressBookEntryTuple addressEntry = (
|
||||
Name: "",
|
||||
World: (int)location.ServerId,
|
||||
City: (int)city,
|
||||
Ward: (int)location.WardId,
|
||||
PropertyType: 1,
|
||||
Plot: 0,
|
||||
Apartment: (int)location.RoomId,
|
||||
ApartmentSubdivision: location.DivisionId == 2,
|
||||
AliasEnabled: false,
|
||||
Alias: ""
|
||||
);
|
||||
return addressEntry;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResidentialAetheryteKind GetResidentialAetheryteKind(TerritoryTypeIdHousing territoryHousing)
|
||||
{
|
||||
return territoryHousing switch
|
||||
{
|
||||
TerritoryTypeIdHousing.Shirogane or
|
||||
TerritoryTypeIdHousing.ShiroganeApartment or
|
||||
TerritoryTypeIdHousing.ShiroganeSmall or
|
||||
TerritoryTypeIdHousing.ShiroganeMedium or
|
||||
TerritoryTypeIdHousing.ShiroganeLarge or
|
||||
TerritoryTypeIdHousing.ShiroganeFCRoom or
|
||||
TerritoryTypeIdHousing.ShiroganeFCWorkshop
|
||||
=> ResidentialAetheryteKind.Kugane,
|
||||
|
||||
TerritoryTypeIdHousing.Lavender or
|
||||
TerritoryTypeIdHousing.LavenderSmall or
|
||||
TerritoryTypeIdHousing.LavenderMedium or
|
||||
TerritoryTypeIdHousing.LavenderLarge or
|
||||
TerritoryTypeIdHousing.LavenderApartment or
|
||||
TerritoryTypeIdHousing.LavenderFCRoom or
|
||||
TerritoryTypeIdHousing.LavenderFCWorkshop
|
||||
=> ResidentialAetheryteKind.Gridania,
|
||||
|
||||
TerritoryTypeIdHousing.Mist or
|
||||
TerritoryTypeIdHousing.MistSmall or
|
||||
TerritoryTypeIdHousing.MistMedium or
|
||||
TerritoryTypeIdHousing.MistLarge or
|
||||
TerritoryTypeIdHousing.MistApartment or
|
||||
TerritoryTypeIdHousing.MistFCRoom or
|
||||
TerritoryTypeIdHousing.MistFCWorkshop
|
||||
=> ResidentialAetheryteKind.Limsa,
|
||||
|
||||
TerritoryTypeIdHousing.Goblet or
|
||||
TerritoryTypeIdHousing.GobletSmall or
|
||||
TerritoryTypeIdHousing.GobletMedium or
|
||||
TerritoryTypeIdHousing.GobletLarge or
|
||||
TerritoryTypeIdHousing.GobletApartment or
|
||||
TerritoryTypeIdHousing.GobletFCRoom or
|
||||
TerritoryTypeIdHousing.GobletFCWorkshop
|
||||
=> ResidentialAetheryteKind.Uldah,
|
||||
|
||||
TerritoryTypeIdHousing.Empyream or
|
||||
TerritoryTypeIdHousing.EmpyreamSmall or
|
||||
TerritoryTypeIdHousing.EmpyreamMedium or
|
||||
TerritoryTypeIdHousing.EmpyreamLarge or
|
||||
TerritoryTypeIdHousing.EmpyreamApartment or
|
||||
TerritoryTypeIdHousing.EmpyreamFCRoom or
|
||||
TerritoryTypeIdHousing.EmpyreamFCWorkshop
|
||||
=> ResidentialAetheryteKind.Foundation,
|
||||
|
||||
_ => ResidentialAetheryteKind.None
|
||||
};
|
||||
}
|
||||
|
||||
public string? GetMapAddressByLocation(LocationInfo location)
|
||||
{
|
||||
string? liString = null;
|
||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
||||
if (GetResidentialAetheryteKind(territoryHousing) == ResidentialAetheryteKind.None)
|
||||
{
|
||||
liString = _dalamudUtilService.LocationToLifestream(location);
|
||||
}
|
||||
return liString;
|
||||
}
|
||||
|
||||
public DateTimeOffset GetSharingStatus(string uid)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -21,6 +21,12 @@ public record SwitchToIntroUiMessage : MessageBase;
|
||||
public record SwitchToMainUiMessage : MessageBase;
|
||||
public record OpenSettingsUiMessage : MessageBase;
|
||||
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||
public enum PerformanceSettingsSection
|
||||
{
|
||||
TextureOptimization,
|
||||
ModelOptimization,
|
||||
}
|
||||
public record OpenPerformanceSettingsMessage(PerformanceSettingsSection Section) : MessageBase;
|
||||
public record DalamudLoginMessage : MessageBase;
|
||||
public record DalamudLogoutMessage : MessageBase;
|
||||
public record ActorTrackedMessage(ActorObjectService.ActorDescriptor Descriptor) : SameThreadMessage;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
132
LightlessSync/Services/ModelDecimation/ModelDecimationFilters.cs
Normal file
132
LightlessSync/Services/ModelDecimation/ModelDecimationFilters.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
namespace LightlessSync.Services.ModelDecimation;
|
||||
|
||||
internal static class ModelDecimationFilters
|
||||
{
|
||||
// MODELS ONLY HERE, NOT MATERIALS
|
||||
internal static readonly string[] HairPaths =
|
||||
[
|
||||
"/hair/",
|
||||
"hir.mdl",
|
||||
];
|
||||
|
||||
internal static readonly string[] ClothingPaths =
|
||||
[
|
||||
"chara/equipment/",
|
||||
"/equipment/",
|
||||
|
||||
"met.mdl",
|
||||
"top.mdl",
|
||||
"glv.mdl",
|
||||
"dwn.mdl",
|
||||
"sho.mdl",
|
||||
];
|
||||
|
||||
internal static readonly string[] AccessoryPaths =
|
||||
[
|
||||
"/accessory/",
|
||||
"chara/accessory/",
|
||||
|
||||
"ear.mdl",
|
||||
"nek.mdl",
|
||||
"wrs.mdl",
|
||||
"ril.mdl",
|
||||
"rir.mdl",
|
||||
];
|
||||
|
||||
internal static readonly string[] BodyPaths =
|
||||
[
|
||||
"/body/",
|
||||
"chara/equipment/e0000/model/",
|
||||
"chara/equipment/e9903/model/",
|
||||
"chara/equipment/e9903/model/",
|
||||
"chara/equipment/e0279/model/",
|
||||
];
|
||||
|
||||
internal static readonly string[] FaceHeadPaths =
|
||||
[
|
||||
"/face/",
|
||||
"/obj/face/",
|
||||
"/head/",
|
||||
"fac.mdl",
|
||||
];
|
||||
|
||||
internal static readonly string[] TailOrEarPaths =
|
||||
[
|
||||
"/tail/",
|
||||
"/obj/tail/",
|
||||
"/zear/",
|
||||
"/obj/zear/",
|
||||
|
||||
"til.mdl",
|
||||
"zer.mdl",
|
||||
];
|
||||
|
||||
// BODY MATERIALS ONLY, NOT MESHES
|
||||
internal static readonly string[] BodyMaterials =
|
||||
[
|
||||
"b0001_bibo.mtrl",
|
||||
"b0101_bibo.mtrl",
|
||||
|
||||
"b0001_a.mtrl",
|
||||
"b0001_b.mtrl",
|
||||
|
||||
"b0101_a.mtrl",
|
||||
"b0101_b.mtrl",
|
||||
];
|
||||
|
||||
internal static string NormalizePath(string path)
|
||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||
|
||||
internal static bool IsHairPath(string normalizedPath)
|
||||
=> ContainsAny(normalizedPath, HairPaths);
|
||||
|
||||
internal static bool IsClothingPath(string normalizedPath)
|
||||
=> ContainsAny(normalizedPath, ClothingPaths);
|
||||
|
||||
internal static bool IsAccessoryPath(string normalizedPath)
|
||||
=> ContainsAny(normalizedPath, AccessoryPaths);
|
||||
|
||||
|
||||
internal static bool IsBodyPath(string normalizedPath)
|
||||
=> ContainsAny(normalizedPath, BodyPaths);
|
||||
|
||||
internal static bool IsFaceHeadPath(string normalizedPath)
|
||||
=> ContainsAny(normalizedPath, FaceHeadPaths);
|
||||
|
||||
internal static bool IsTailOrEarPath(string normalizedPath)
|
||||
=> ContainsAny(normalizedPath, TailOrEarPaths);
|
||||
|
||||
internal static bool ContainsAny(string normalizedPath, IReadOnlyList<string> markers)
|
||||
{
|
||||
for (var i = 0; i < markers.Count; i++)
|
||||
{
|
||||
if (normalizedPath.Contains(markers[i], StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsBodyMaterial(string materialPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(materialPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = NormalizePath(materialPath);
|
||||
var nameStart = normalized.LastIndexOf('/');
|
||||
var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized;
|
||||
foreach (var marker in BodyMaterials)
|
||||
{
|
||||
if (fileName.Contains(marker, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
@@ -19,7 +21,7 @@ public sealed class ModelDecimationService
|
||||
private readonly XivDataStorageService _xivDataStorageService;
|
||||
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TaskRegistry<string> _decimationDeduplicator = new();
|
||||
private readonly ConcurrentDictionary<string, string> _decimatedPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, byte> _failedHashes = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -44,14 +46,14 @@ public sealed class ModelDecimationService
|
||||
return;
|
||||
}
|
||||
|
||||
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _activeJobs.ContainsKey(hash))
|
||||
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _decimationDeduplicator.TryGetExisting(hash, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Queued model decimation for {Hash}", hash);
|
||||
_logger.LogDebug("Queued model decimation for {Hash}", hash);
|
||||
|
||||
_activeJobs[hash] = Task.Run(async () =>
|
||||
_decimationDeduplicator.GetOrStart(hash, async () =>
|
||||
{
|
||||
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
@@ -66,16 +68,54 @@ public sealed class ModelDecimationService
|
||||
finally
|
||||
{
|
||||
_decimationSemaphore.Release();
|
||||
_activeJobs.TryRemove(hash, out _);
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -116,7 +156,7 @@ public sealed class ModelDecimationService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_activeJobs.TryGetValue(hash, out var job))
|
||||
if (_decimationDeduplicator.TryGetExisting(hash, out var job))
|
||||
{
|
||||
pending.Add(job);
|
||||
}
|
||||
@@ -131,6 +171,23 @@ public sealed class ModelDecimationService
|
||||
}
|
||||
|
||||
private Task DecimateInternalAsync(string hash, string sourcePath)
|
||||
{
|
||||
if (!TryGetDecimationSettings(out var settings))
|
||||
{
|
||||
_logger.LogDebug("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,30 +196,48 @@ public sealed class ModelDecimationService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio))
|
||||
if (!TryNormalizeSettings(settings, out var normalized))
|
||||
{
|
||||
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
|
||||
_logger.LogDebug("Model decimation skipped for {Hash}; invalid settings.", hash);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##})", hash, triangleThreshold, targetRatio);
|
||||
_logger.LogDebug(
|
||||
"Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents}, avoid body intersection {AvoidBodyIntersection})",
|
||||
hash,
|
||||
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, _logger))
|
||||
if (!MdlDecimator.TryDecimate(sourcePath, destination, normalized, _logger))
|
||||
{
|
||||
_failedHashes[hash] = 1;
|
||||
_logger.LogInformation("Model decimation skipped for {Hash}", hash);
|
||||
_logger.LogDebug("Model decimation skipped for {Hash}", hash);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||
_logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination);
|
||||
if (registerDecimatedPath)
|
||||
{
|
||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||
}
|
||||
_logger.LogDebug("Decimated model {Hash} -> {Path}", hash, destination);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -250,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))
|
||||
{
|
||||
@@ -262,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;
|
||||
}
|
||||
|
||||
@@ -273,50 +348,48 @@ public sealed class ModelDecimationService
|
||||
return true;
|
||||
}
|
||||
|
||||
var normalized = NormalizeGamePath(gamePath);
|
||||
if (normalized.Contains("/hair/", StringComparison.Ordinal))
|
||||
var normalized = ModelDecimationFilters.NormalizePath(gamePath);
|
||||
if (ModelDecimationFilters.IsHairPath(normalized))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsClothingPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsAccessoryPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsBodyPath(normalized))
|
||||
{
|
||||
if (normalized.Contains("/body/", StringComparison.Ordinal))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
||||
}
|
||||
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||
}
|
||||
if (ModelDecimationFilters.IsFaceHeadPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/tail/", StringComparison.Ordinal))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||
}
|
||||
if (ModelDecimationFilters.IsTailOrEarPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string NormalizeGamePath(string path)
|
||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||
|
||||
private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio)
|
||||
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
||||
{
|
||||
triangleThreshold = 15_000;
|
||||
targetRatio = 0.8;
|
||||
settings = new ModelDecimationSettings(
|
||||
ModelDecimationDefaults.TriangleThreshold,
|
||||
ModelDecimationDefaults.TargetRatio,
|
||||
ModelDecimationDefaults.NormalizeTangents,
|
||||
ModelDecimationDefaults.AvoidBodyIntersection,
|
||||
new ModelDecimationAdvancedSettings());
|
||||
|
||||
var config = _performanceConfigService.Current;
|
||||
if (!config.EnableModelDecimation)
|
||||
@@ -324,14 +397,86 @@ public sealed class ModelDecimationService
|
||||
return false;
|
||||
}
|
||||
|
||||
triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
|
||||
targetRatio = config.ModelDecimationTargetRatio;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -1,4 +1,6 @@
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using System.Linq;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -8,14 +10,18 @@ namespace LightlessSync.Services;
|
||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly IpcManager _ipc;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly TempCollectionConfigService _config;
|
||||
private readonly CancellationTokenSource _cleanupCts = new();
|
||||
private int _ran;
|
||||
private const int CleanupBatchSize = 50;
|
||||
private static readonly TimeSpan CleanupBatchDelay = TimeSpan.FromMilliseconds(50);
|
||||
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
|
||||
|
||||
public PenumbraTempCollectionJanitor(
|
||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||
LightlessMediator mediator,
|
||||
IpcManager ipc,
|
||||
LightlessConfigService config) : base(logger, mediator)
|
||||
TempCollectionConfigService config) : base(logger, mediator)
|
||||
{
|
||||
_ipc = ipc;
|
||||
_config = config;
|
||||
@@ -26,15 +32,41 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
public void Register(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty) return;
|
||||
if (_config.Current.OrphanableTempCollections.Add(id))
|
||||
var changed = false;
|
||||
var config = _config.Current;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
|
||||
if (existing is null)
|
||||
{
|
||||
config.OrphanableTempCollectionEntries.Add(new OrphanableTempCollectionEntry
|
||||
{
|
||||
Id = id,
|
||||
RegisteredAtUtc = now
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
else if (existing.RegisteredAtUtc == DateTime.MinValue)
|
||||
{
|
||||
existing.RegisteredAtUtc = now;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
_config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty) return;
|
||||
if (_config.Current.OrphanableTempCollections.Remove(id))
|
||||
var config = _config.Current;
|
||||
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
||||
if (changed)
|
||||
{
|
||||
_config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupOrphansOnBoot()
|
||||
@@ -45,27 +77,139 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
if (!_ipc.Penumbra.APIAvailable)
|
||||
return;
|
||||
|
||||
var ids = _config.Current.OrphanableTempCollections.ToArray();
|
||||
if (ids.Length == 0)
|
||||
return;
|
||||
|
||||
var appId = Guid.NewGuid();
|
||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length);
|
||||
|
||||
foreach (var id in ids)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
||||
.GetAwaiter().GetResult();
|
||||
await CleanupOrphansOnBootAsync(_cleanupCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error cleaning orphaned temp collections");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task CleanupOrphansOnBootAsync(CancellationToken token)
|
||||
{
|
||||
var config = _config.Current;
|
||||
var entries = config.OrphanableTempCollectionEntries;
|
||||
if (entries.Count == 0)
|
||||
return;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var changed = EnsureEntryTimes(entries, now);
|
||||
var cutoff = now - OrphanCleanupDelay;
|
||||
var expired = entries
|
||||
.Where(entry => entry.Id != Guid.Empty && entry.RegisteredAtUtc != DateTime.MinValue && entry.RegisteredAtUtc <= cutoff)
|
||||
.Select(entry => entry.Id)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (expired.Count == 0)
|
||||
{
|
||||
if (changed)
|
||||
{
|
||||
_config.Save();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var appId = Guid.NewGuid();
|
||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
|
||||
|
||||
List<Guid> removedIds = [];
|
||||
foreach (var id in expired)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
||||
}
|
||||
|
||||
removedIds.Add(id);
|
||||
if (removedIds.Count % CleanupBatchSize == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(CleanupBatchDelay, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removedIds.Count == 0)
|
||||
{
|
||||
if (changed)
|
||||
{
|
||||
_config.Save();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var id in removedIds)
|
||||
{
|
||||
RemoveEntry(entries, id);
|
||||
}
|
||||
|
||||
_config.Current.OrphanableTempCollections.Clear();
|
||||
_config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_cleanupCts.Cancel();
|
||||
_cleanupCts.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private static int RemoveEntry(List<OrphanableTempCollectionEntry> entries, Guid id)
|
||||
{
|
||||
var removed = 0;
|
||||
for (var i = entries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (entries[i].Id != id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.RemoveAt(i);
|
||||
removed++;
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
private static bool EnsureEntryTimes(List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||
{
|
||||
var changed = false;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.RegisteredAtUtc = now;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,10 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
DrawSeparator(sb, longestCounterName);
|
||||
}
|
||||
|
||||
var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : [.. entry.Value];
|
||||
var snapshot = entry.Value.Snapshot();
|
||||
var pastEntries = limitBySeconds > 0
|
||||
? snapshot.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList()
|
||||
: snapshot;
|
||||
|
||||
if (pastEntries.Any())
|
||||
{
|
||||
@@ -189,7 +192,11 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
{
|
||||
try
|
||||
{
|
||||
var last = entries.Value.ToList()[^1];
|
||||
if (!entries.Value.TryGetLast(out var last))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
|
||||
{
|
||||
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key);
|
||||
|
||||
@@ -2,6 +2,7 @@ using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.FileCache;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LightlessSync.Services.TextureCompression;
|
||||
|
||||
@@ -27,7 +28,9 @@ public sealed class TextureCompressionService
|
||||
public async Task ConvertTexturesAsync(
|
||||
IReadOnlyList<TextureCompressionRequest> requests,
|
||||
IProgress<TextureConversionProgress>? progress,
|
||||
CancellationToken token)
|
||||
CancellationToken token,
|
||||
bool requestRedraw = true,
|
||||
bool includeMipMaps = true)
|
||||
{
|
||||
if (requests.Count == 0)
|
||||
{
|
||||
@@ -48,7 +51,7 @@ public sealed class TextureCompressionService
|
||||
continue;
|
||||
}
|
||||
|
||||
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token).ConfigureAwait(false);
|
||||
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token, requestRedraw, includeMipMaps).ConfigureAwait(false);
|
||||
|
||||
completed++;
|
||||
}
|
||||
@@ -65,14 +68,16 @@ public sealed class TextureCompressionService
|
||||
int total,
|
||||
int completedBefore,
|
||||
IProgress<TextureConversionProgress>? progress,
|
||||
CancellationToken token)
|
||||
CancellationToken token,
|
||||
bool requestRedraw,
|
||||
bool includeMipMaps)
|
||||
{
|
||||
var primaryPath = request.PrimaryFilePath;
|
||||
var displayJob = new TextureConversionJob(
|
||||
primaryPath,
|
||||
primaryPath,
|
||||
targetType,
|
||||
IncludeMipMaps: true,
|
||||
IncludeMipMaps: includeMipMaps,
|
||||
request.DuplicateFilePaths);
|
||||
|
||||
var backupPath = CreateBackupCopy(primaryPath);
|
||||
@@ -83,7 +88,7 @@ public sealed class TextureCompressionService
|
||||
try
|
||||
{
|
||||
WaitForAccess(primaryPath);
|
||||
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token, requestRedraw).ConfigureAwait(false);
|
||||
|
||||
if (!IsValidConversionResult(displayJob.OutputFile))
|
||||
{
|
||||
@@ -128,19 +133,46 @@ public sealed class TextureCompressionService
|
||||
var cacheEntries = _fileCacheManager.GetFileCachesByPaths(paths.ToArray());
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var hasExpectedHash = TryGetExpectedHashFromPath(path, out var expectedHash);
|
||||
if (!cacheEntries.TryGetValue(path, out var entry) || entry is null)
|
||||
{
|
||||
entry = _fileCacheManager.CreateFileEntry(path);
|
||||
if (hasExpectedHash)
|
||||
{
|
||||
entry = _fileCacheManager.CreateCacheEntryWithKnownHash(path, expectedHash);
|
||||
}
|
||||
|
||||
entry ??= _fileCacheManager.CreateFileEntry(path);
|
||||
if (entry is null)
|
||||
{
|
||||
_logger.LogWarning("Unable to locate cache entry for {Path}; skipping hash refresh", path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (hasExpectedHash && entry.IsCacheEntry && !string.Equals(entry.Hash, expectedHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Fixing cache hash mismatch for {Path}: {Current} -> {Expected}", path, entry.Hash, expectedHash);
|
||||
_fileCacheManager.RemoveHashedFile(entry.Hash, entry.PrefixedFilePath, removeDerivedFiles: false);
|
||||
var corrected = _fileCacheManager.CreateCacheEntryWithKnownHash(path, expectedHash);
|
||||
if (corrected is not null)
|
||||
{
|
||||
entry = corrected;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_fileCacheManager.UpdateHashedFile(entry);
|
||||
if (entry.IsCacheEntry)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
entry.Size = info.Length;
|
||||
entry.CompressedSize = null;
|
||||
entry.LastModifiedDateTicks = info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||
_fileCacheManager.UpdateHashedFile(entry, computeProperties: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_fileCacheManager.UpdateHashedFile(entry);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -149,6 +181,35 @@ public sealed class TextureCompressionService
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetExpectedHashFromPath(string path, out string hash)
|
||||
{
|
||||
hash = Path.GetFileNameWithoutExtension(path);
|
||||
if (string.IsNullOrWhiteSpace(hash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hash.Length is not (40 or 64))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
var c = hash[i];
|
||||
var isHex = (c >= '0' && c <= '9')
|
||||
|| (c >= 'a' && c <= 'f')
|
||||
|| (c >= 'A' && c <= 'F');
|
||||
if (!isHex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
hash = hash.ToUpperInvariant();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly string WorkingDirectory =
|
||||
Path.Combine(Path.GetTempPath(), "LightlessSync.TextureCompression");
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using OtterTex;
|
||||
using OtterImage = OtterTex.Image;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.FileCache;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Lumina.Data.Files;
|
||||
@@ -30,10 +32,12 @@ public sealed class TextureDownscaleService
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly TextureCompressionService _textureCompressionService;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TaskRegistry<string> _downscaleDeduplicator = new();
|
||||
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
|
||||
private readonly SemaphoreSlim _compressionSemaphore = new(1);
|
||||
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
|
||||
new Dictionary<int, TextureCompressionTarget>
|
||||
{
|
||||
@@ -68,12 +72,14 @@ public sealed class TextureDownscaleService
|
||||
ILogger<TextureDownscaleService> logger,
|
||||
LightlessConfigService configService,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||
FileCacheManager fileCacheManager)
|
||||
FileCacheManager fileCacheManager,
|
||||
TextureCompressionService textureCompressionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_textureCompressionService = textureCompressionService;
|
||||
}
|
||||
|
||||
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
||||
@@ -82,9 +88,9 @@ public sealed class TextureDownscaleService
|
||||
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
|
||||
{
|
||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (_activeJobs.ContainsKey(hash)) return;
|
||||
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
|
||||
|
||||
_activeJobs[hash] = Task.Run(async () =>
|
||||
_downscaleDeduplicator.GetOrStart(hash, async () =>
|
||||
{
|
||||
TextureMapKind mapKind;
|
||||
try
|
||||
@@ -98,7 +104,7 @@ public sealed class TextureDownscaleService
|
||||
}
|
||||
|
||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||
}, CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShouldScheduleDownscale(string filePath)
|
||||
@@ -107,7 +113,9 @@ public sealed class TextureDownscaleService
|
||||
return false;
|
||||
|
||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||
return performanceConfig.EnableNonIndexTextureMipTrim || performanceConfig.EnableIndexTextureDownscale;
|
||||
return performanceConfig.EnableNonIndexTextureMipTrim
|
||||
|| performanceConfig.EnableIndexTextureDownscale
|
||||
|| performanceConfig.EnableUncompressedTextureCompression;
|
||||
}
|
||||
|
||||
public string GetPreferredPath(string hash, string originalPath)
|
||||
@@ -144,7 +152,7 @@ public sealed class TextureDownscaleService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_activeJobs.TryGetValue(hash, out var job))
|
||||
if (_downscaleDeduplicator.TryGetExisting(hash, out var job))
|
||||
{
|
||||
pending.Add(job);
|
||||
}
|
||||
@@ -182,10 +190,18 @@ public sealed class TextureDownscaleService
|
||||
targetMaxDimension = ResolveTargetMaxDimension();
|
||||
onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures;
|
||||
|
||||
if (onlyDownscaleUncompressed && !headerInfo.HasValue)
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for texture {Hash}; format unknown and only-uncompressed enabled.", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
|
||||
if (File.Exists(destination))
|
||||
{
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -196,6 +212,7 @@ public sealed class TextureDownscaleService
|
||||
if (performanceConfig.EnableNonIndexTextureMipTrim
|
||||
&& await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false))
|
||||
{
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,6 +223,7 @@ public sealed class TextureDownscaleService
|
||||
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,6 +231,7 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,6 +241,7 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,10 +249,12 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using var sourceScratch = TexFileHelper.Load(sourcePath);
|
||||
var sourceFormat = sourceScratch.Meta.Format;
|
||||
using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);
|
||||
|
||||
var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8;
|
||||
@@ -248,16 +270,39 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
||||
|
||||
var canReencodeWithPenumbra = TryResolveCompressionTarget(headerInfo, sourceFormat, out var compressionTarget);
|
||||
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
|
||||
using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
if (!TryConvertForSave(resizedScratch, sourceFormat, out var finalScratch, canReencodeWithPenumbra))
|
||||
{
|
||||
if (canReencodeWithPenumbra
|
||||
&& await TryReencodeWithPenumbraAsync(hash, sourcePath, destination, resizedScratch, compressionTarget).ConfigureAwait(false))
|
||||
{
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
TexFileHelper.Save(destination, finalScratch);
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace(
|
||||
"Skipping downscale for index texture {Hash}; failed to re-encode to {Format}.",
|
||||
hash,
|
||||
sourceFormat);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using (finalScratch)
|
||||
{
|
||||
TexFileHelper.Save(destination, finalScratch);
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
}
|
||||
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -277,7 +322,6 @@ public sealed class TextureDownscaleService
|
||||
finally
|
||||
{
|
||||
_downscaleSemaphore.Release();
|
||||
_activeJobs.TryRemove(hash, out _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +374,157 @@ public sealed class TextureDownscaleService
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryConvertForSave(
|
||||
ScratchImage source,
|
||||
DXGIFormat sourceFormat,
|
||||
out ScratchImage result,
|
||||
bool attemptPenumbraFallback)
|
||||
{
|
||||
var isCompressed = sourceFormat.IsCompressed();
|
||||
var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm;
|
||||
try
|
||||
{
|
||||
result = source.Convert(targetFormat);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var compressedFallback = attemptPenumbraFallback
|
||||
? " Attempting Penumbra re-encode."
|
||||
: " Skipping downscale.";
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Failed to convert downscaled texture to {Format}.{Fallback}",
|
||||
targetFormat,
|
||||
isCompressed ? compressedFallback : " Falling back to B8G8R8A8.");
|
||||
if (isCompressed)
|
||||
{
|
||||
result = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = source.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveCompressionTarget(TexHeaderInfo? headerInfo, DXGIFormat sourceFormat, out TextureCompressionTarget target)
|
||||
{
|
||||
if (headerInfo is { } info && TryGetCompressionTarget(info.Format, out target))
|
||||
{
|
||||
return _textureCompressionService.IsTargetSelectable(target);
|
||||
}
|
||||
|
||||
if (sourceFormat.IsCompressed() && BlockCompressedFormatMap.TryGetValue((int)sourceFormat, out target))
|
||||
{
|
||||
return _textureCompressionService.IsTargetSelectable(target);
|
||||
}
|
||||
|
||||
target = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> TryReencodeWithPenumbraAsync(
|
||||
string hash,
|
||||
string sourcePath,
|
||||
string destination,
|
||||
ScratchImage resizedScratch,
|
||||
TextureCompressionTarget target)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
TexFileHelper.Save(destination, uncompressed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to save uncompressed downscaled texture for {Hash}. Skipping downscale.", hash);
|
||||
TryDelete(destination);
|
||||
return false;
|
||||
}
|
||||
|
||||
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var request = new TextureCompressionRequest(destination, Array.Empty<string>(), target);
|
||||
await _textureCompressionService
|
||||
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to re-encode downscaled texture {Hash} to {Target}. Skipping downscale.", hash, target);
|
||||
TryDelete(destination);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_compressionSemaphore.Release();
|
||||
}
|
||||
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
_logger.LogDebug("Downscaled texture {Hash} -> {Path} (re-encoded via Penumbra).", hash, destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task TryAutoCompressAsync(string hash, string texturePath, TextureMapKind mapKind, TexHeaderInfo? headerInfo)
|
||||
{
|
||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||
if (!performanceConfig.EnableUncompressedTextureCompression)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(texturePath) || !File.Exists(texturePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var info = headerInfo ?? (TryReadTexHeader(texturePath, out var header) ? header : (TexHeaderInfo?)null);
|
||||
if (!info.HasValue)
|
||||
{
|
||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; unable to read header.", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsBlockCompressedFormat(info.Value.Format))
|
||||
{
|
||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; already block-compressed.", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
var suggestion = TextureMetadataHelper.GetSuggestedTarget(info.Value.Format.ToString(), mapKind, texturePath);
|
||||
if (suggestion is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = _textureCompressionService.NormalizeTarget(suggestion.Value.Target);
|
||||
if (!_textureCompressionService.IsTargetSelectable(target))
|
||||
{
|
||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; target {Target} not supported.", hash, target);
|
||||
return;
|
||||
}
|
||||
|
||||
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var includeMipMaps = !performanceConfig.SkipUncompressedTextureCompressionMipMaps;
|
||||
var request = new TextureCompressionRequest(texturePath, Array.Empty<string>(), target);
|
||||
await _textureCompressionService
|
||||
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false, includeMipMaps: includeMipMaps)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Auto-compress failed for texture {Hash} ({Path})", hash, texturePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_compressionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsIndexMap(TextureMapKind kind)
|
||||
=> kind is TextureMapKind.Mask
|
||||
or TextureMapKind.Index;
|
||||
|
||||
@@ -13,16 +13,20 @@ namespace LightlessSync.Services;
|
||||
public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly List<WindowMediatorSubscriberBase> _createdWindows = [];
|
||||
private readonly List<WindowMediatorSubscriberBase> _registeredWindows = [];
|
||||
private readonly HashSet<WindowMediatorSubscriberBase> _uiHiddenWindows = [];
|
||||
private readonly IUiBuilder _uiBuilder;
|
||||
private readonly FileDialogManager _fileDialogManager;
|
||||
private readonly ILogger<UiService> _logger;
|
||||
private readonly LightlessConfigService _lightlessConfigService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly WindowSystem _windowSystem;
|
||||
private readonly UiFactory _uiFactory;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private bool _uiHideActive;
|
||||
|
||||
public UiService(ILogger<UiService> logger, IUiBuilder uiBuilder,
|
||||
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
|
||||
LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService, WindowSystem windowSystem,
|
||||
IEnumerable<WindowMediatorSubscriberBase> windows,
|
||||
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
||||
LightlessMediator lightlessMediator, PairFactory pairFactory) : base(logger, lightlessMediator)
|
||||
@@ -31,6 +35,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
_logger.LogTrace("Creating {type}", GetType().Name);
|
||||
_uiBuilder = uiBuilder;
|
||||
_lightlessConfigService = lightlessConfigService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_windowSystem = windowSystem;
|
||||
_uiFactory = uiFactory;
|
||||
_pairFactory = pairFactory;
|
||||
@@ -43,6 +48,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
|
||||
foreach (var window in windows)
|
||||
{
|
||||
_registeredWindows.Add(window);
|
||||
_windowSystem.AddWindow(window);
|
||||
}
|
||||
|
||||
@@ -176,6 +182,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
_windowSystem.RemoveWindow(msg.Window);
|
||||
_createdWindows.Remove(msg.Window);
|
||||
_registeredWindows.Remove(msg.Window);
|
||||
_uiHiddenWindows.Remove(msg.Window);
|
||||
msg.Window.Dispose();
|
||||
});
|
||||
}
|
||||
@@ -219,12 +227,72 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
MainStyle.PushStyle();
|
||||
try
|
||||
{
|
||||
var hideOtherUi = ShouldHideOtherUi();
|
||||
UpdateUiHideState(hideOtherUi);
|
||||
_windowSystem.Draw();
|
||||
_fileDialogManager.Draw();
|
||||
if (!hideOtherUi)
|
||||
_fileDialogManager.Draw();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainStyle.PopStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldHideOtherUi()
|
||||
{
|
||||
var config = _lightlessConfigService.Current;
|
||||
if (!config.ShowUiWhenUiHidden && _dalamudUtilService.IsGameUiHidden)
|
||||
return true;
|
||||
|
||||
if (!config.ShowUiInGpose && _dalamudUtilService.IsInGpose)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateUiHideState(bool hideOtherUi)
|
||||
{
|
||||
if (!hideOtherUi)
|
||||
{
|
||||
if (_uiHideActive)
|
||||
{
|
||||
foreach (var window in _uiHiddenWindows)
|
||||
{
|
||||
window.IsOpen = true;
|
||||
}
|
||||
|
||||
_uiHiddenWindows.Clear();
|
||||
_uiHideActive = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_uiHideActive = true;
|
||||
foreach (var window in EnumerateManagedWindows())
|
||||
{
|
||||
if (window is ZoneChatUi)
|
||||
continue;
|
||||
|
||||
if (!window.IsOpen)
|
||||
continue;
|
||||
|
||||
_uiHiddenWindows.Add(window);
|
||||
window.IsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<WindowMediatorSubscriberBase> EnumerateManagedWindows()
|
||||
{
|
||||
foreach (var window in _registeredWindows)
|
||||
{
|
||||
yield return window;
|
||||
}
|
||||
|
||||
foreach (var window in _createdWindows)
|
||||
{
|
||||
yield return window;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,6 +480,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<long> GetEffectiveTrianglesByHash(string hash, string filePath)
|
||||
{
|
||||
if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A decimation algorithm.
|
||||
/// </summary>
|
||||
public abstract class DecimationAlgorithm
|
||||
{
|
||||
#region Delegates
|
||||
/// <summary>
|
||||
/// A callback for decimation status reports.
|
||||
/// </summary>
|
||||
/// <param name="iteration">The current iteration, starting at zero.</param>
|
||||
/// <param name="originalTris">The original count of triangles.</param>
|
||||
/// <param name="currentTris">The current count of triangles.</param>
|
||||
/// <param name="targetTris">The target count of triangles.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Gets or sets if borders should be kept.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
[Obsolete("Use the 'DecimationAlgorithm.PreserveBorders' property instead.", false)]
|
||||
public bool KeepBorders
|
||||
{
|
||||
get { return preserveBorders; }
|
||||
set { preserveBorders = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if borders should be preserved.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
public bool PreserveBorders
|
||||
{
|
||||
get { return preserveBorders; }
|
||||
set { preserveBorders = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if linked vertices should be kept.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
[Obsolete("This feature has been removed, for more details why please read the readme.", true)]
|
||||
public bool KeepLinkedVertices
|
||||
{
|
||||
get { return false; }
|
||||
set { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum vertex count. Set to zero for no limitation.
|
||||
/// Default value: 0 (no limitation)
|
||||
/// </summary>
|
||||
public int MaxVertexCount
|
||||
{
|
||||
get { return maxVertexCount; }
|
||||
set { maxVertexCount = Math.MathHelper.Max(value, 0); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if verbose information should be printed in the console.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
public bool Verbose
|
||||
{
|
||||
get { return verbose; }
|
||||
set { verbose = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger used for diagnostics.
|
||||
/// </summary>
|
||||
public ILogger? Logger { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// An event for status reports for this algorithm.
|
||||
/// </summary>
|
||||
public event StatusReportCallback StatusReport
|
||||
{
|
||||
add { statusReportInvoker += value; }
|
||||
remove { statusReportInvoker -= value; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Protected Methods
|
||||
/// <summary>
|
||||
/// Reports the current status of the decimation.
|
||||
/// </summary>
|
||||
/// <param name="iteration">The current iteration, starting at zero.</param>
|
||||
/// <param name="originalTris">The original count of triangles.</param>
|
||||
/// <param name="currentTris">The current count of triangles.</param>
|
||||
/// <param name="targetTris">The target count of triangles.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Initializes the algorithm with the original mesh.
|
||||
/// </summary>
|
||||
/// <param name="mesh">The mesh.</param>
|
||||
public abstract void Initialize(Mesh mesh);
|
||||
|
||||
/// <summary>
|
||||
/// Decimates the mesh.
|
||||
/// </summary>
|
||||
/// <param name="targetTrisCount">The target triangle count.</param>
|
||||
public abstract void DecimateMesh(int targetTrisCount);
|
||||
|
||||
/// <summary>
|
||||
/// Decimates the mesh without losing any quality.
|
||||
/// </summary>
|
||||
public abstract void DecimateMeshLossless();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the resulting mesh.
|
||||
/// </summary>
|
||||
/// <returns>The resulting mesh.</returns>
|
||||
public abstract Mesh ToMesh();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A bone weight.
|
||||
/// </summary>
|
||||
public struct BoneWeight : IEquatable<BoneWeight>
|
||||
{
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The first bone index.
|
||||
/// </summary>
|
||||
public int boneIndex0;
|
||||
/// <summary>
|
||||
/// The second bone index.
|
||||
/// </summary>
|
||||
public int boneIndex1;
|
||||
/// <summary>
|
||||
/// The third bone index.
|
||||
/// </summary>
|
||||
public int boneIndex2;
|
||||
/// <summary>
|
||||
/// The fourth bone index.
|
||||
/// </summary>
|
||||
public int boneIndex3;
|
||||
|
||||
/// <summary>
|
||||
/// The first bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight0;
|
||||
/// <summary>
|
||||
/// The second bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight1;
|
||||
/// <summary>
|
||||
/// The third bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight2;
|
||||
/// <summary>
|
||||
/// The fourth bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight3;
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new bone weight.
|
||||
/// </summary>
|
||||
/// <param name="boneIndex0">The first bone index.</param>
|
||||
/// <param name="boneIndex1">The second bone index.</param>
|
||||
/// <param name="boneIndex2">The third bone index.</param>
|
||||
/// <param name="boneIndex3">The fourth bone index.</param>
|
||||
/// <param name="boneWeight0">The first bone weight.</param>
|
||||
/// <param name="boneWeight1">The second bone weight.</param>
|
||||
/// <param name="boneWeight2">The third bone weight.</param>
|
||||
/// <param name="boneWeight3">The fourth bone weight.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns if two bone weights equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side bone weight.</param>
|
||||
/// <param name="rhs">The right hand side bone weight.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two bone weights don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side bone weight.</param>
|
||||
/// <param name="rhs">The right hand side bone weight.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this bone weight is equal to another object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The other object to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this bone weight is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other bone weight to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this bone weight.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Merges two bone weights and stores the merged result in the first parameter.
|
||||
/// </summary>
|
||||
/// <param name="a">The first bone weight, also stores result.</param>
|
||||
/// <param name="b">The second bone weight.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A resizable array.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The item type.</typeparam>
|
||||
internal sealed class ResizableArray<T>
|
||||
{
|
||||
#region Fields
|
||||
private T[] items = null;
|
||||
private int length = 0;
|
||||
|
||||
private static T[] emptyArr = new T[0];
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the length of this array.
|
||||
/// </summary>
|
||||
public int Length
|
||||
{
|
||||
get { return length; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the internal data buffer for this array.
|
||||
/// </summary>
|
||||
public T[] Data
|
||||
{
|
||||
get { return items; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the element value at a specific index.
|
||||
/// </summary>
|
||||
/// <param name="index">The element index.</param>
|
||||
/// <returns>The element value.</returns>
|
||||
public T this[int index]
|
||||
{
|
||||
get { return items[index]; }
|
||||
set { items[index] = value; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new resizable array.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial array capacity.</param>
|
||||
public ResizableArray(int capacity)
|
||||
: this(capacity, 0)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new resizable array.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial array capacity.</param>
|
||||
/// <param name="length">The initial length of the array.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Clears this array.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(items, 0, length);
|
||||
length = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes this array.
|
||||
/// </summary>
|
||||
/// <param name="length">The new length.</param>
|
||||
/// <param name="trimExess">If exess memory should be trimmed.</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trims any excess memory for this array.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new item to the end of this array.
|
||||
/// </summary>
|
||||
/// <param name="item">The new item.</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
if (length >= items.Length)
|
||||
{
|
||||
IncreaseCapacity(items.Length << 1);
|
||||
}
|
||||
|
||||
items[length++] = item;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace MeshDecimator.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of UV channels.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVec">The UV vector type.</typeparam>
|
||||
internal sealed class UVChannels<TVec>
|
||||
{
|
||||
#region Fields
|
||||
private ResizableArray<TVec>[] channels = null;
|
||||
private TVec[][] channelsData = null;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the channel collection data.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific channel by index.
|
||||
/// </summary>
|
||||
/// <param name="index">The channel index.</param>
|
||||
public ResizableArray<TVec> this[int index]
|
||||
{
|
||||
get { return channels[index]; }
|
||||
set { channels[index] = value; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new collection of UV channels.
|
||||
/// </summary>
|
||||
public UVChannels()
|
||||
{
|
||||
channels = new ResizableArray<TVec>[Mesh.UVChannelCount];
|
||||
channelsData = new TVec[Mesh.UVChannelCount][];
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Resizes all channels at once.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The new capacity.</param>
|
||||
/// <param name="trimExess">If exess memory should be trimmed.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Math helpers.
|
||||
/// </summary>
|
||||
public static class MathHelper
|
||||
{
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The Pi constant.
|
||||
/// </summary>
|
||||
public const float PI = 3.14159274f;
|
||||
|
||||
/// <summary>
|
||||
/// The Pi constant.
|
||||
/// </summary>
|
||||
public const double PId = 3.1415926535897932384626433832795;
|
||||
|
||||
/// <summary>
|
||||
/// Degrees to radian constant.
|
||||
/// </summary>
|
||||
public const float Deg2Rad = PI / 180f;
|
||||
|
||||
/// <summary>
|
||||
/// Degrees to radian constant.
|
||||
/// </summary>
|
||||
public const double Deg2Radd = PId / 180.0;
|
||||
|
||||
/// <summary>
|
||||
/// Radians to degrees constant.
|
||||
/// </summary>
|
||||
public const float Rad2Deg = 180f / PI;
|
||||
|
||||
/// <summary>
|
||||
/// Radians to degrees constant.
|
||||
/// </summary>
|
||||
public const double Rad2Degd = 180.0 / PId;
|
||||
#endregion
|
||||
|
||||
#region Min
|
||||
/// <summary>
|
||||
/// Returns the minimum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static int Min(int val1, int val2)
|
||||
{
|
||||
return (val1 < val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static int Min(int val1, int val2, int val3)
|
||||
{
|
||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static float Min(float val1, float val2)
|
||||
{
|
||||
return (val1 < val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static float Min(float val1, float val2, float val3)
|
||||
{
|
||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static double Min(double val1, double val2)
|
||||
{
|
||||
return (val1 < val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static double Min(double val1, double val2, double val3)
|
||||
{
|
||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Max
|
||||
/// <summary>
|
||||
/// Returns the maximum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static int Max(int val1, int val2)
|
||||
{
|
||||
return (val1 > val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static int Max(int val1, int val2, int val3)
|
||||
{
|
||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static float Max(float val1, float val2)
|
||||
{
|
||||
return (val1 > val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static float Max(float val1, float val2, float val3)
|
||||
{
|
||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static double Max(double val1, double val2)
|
||||
{
|
||||
return (val1 > val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static double Max(double val1, double val2, double val3)
|
||||
{
|
||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Clamping
|
||||
/// <summary>
|
||||
/// Clamps a value between a minimum and a maximum value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static float Clamp(float value, float min, float max)
|
||||
{
|
||||
return (value >= min ? (value <= max ? value : max) : min);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps a value between a minimum and a maximum value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static double Clamp(double value, double min, double max)
|
||||
{
|
||||
return (value >= min ? (value <= max ? value : max) : min);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the value between 0 and 1.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static float Clamp01(float value)
|
||||
{
|
||||
return (value > 0f ? (value < 1f ? value : 1f) : 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the value between 0 and 1.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static double Clamp01(double value)
|
||||
{
|
||||
return (value > 0.0 ? (value < 1.0 ? value : 1.0) : 0.0);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Triangle Area
|
||||
/// <summary>
|
||||
/// Calculates the area of a triangle.
|
||||
/// </summary>
|
||||
/// <param name="p0">The first point.</param>
|
||||
/// <param name="p1">The second point.</param>
|
||||
/// <param name="p2">The third point.</param>
|
||||
/// <returns>The triangle area.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the area of a triangle.
|
||||
/// </summary>
|
||||
/// <param name="p0">The first point.</param>
|
||||
/// <param name="p1">The second point.</param>
|
||||
/// <param name="p2">The third point.</param>
|
||||
/// <returns>The triangle area.</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A symmetric matrix.
|
||||
/// </summary>
|
||||
public struct SymmetricMatrix
|
||||
{
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The m11 component.
|
||||
/// </summary>
|
||||
public double m0;
|
||||
/// <summary>
|
||||
/// The m12 component.
|
||||
/// </summary>
|
||||
public double m1;
|
||||
/// <summary>
|
||||
/// The m13 component.
|
||||
/// </summary>
|
||||
public double m2;
|
||||
/// <summary>
|
||||
/// The m14 component.
|
||||
/// </summary>
|
||||
public double m3;
|
||||
/// <summary>
|
||||
/// The m22 component.
|
||||
/// </summary>
|
||||
public double m4;
|
||||
/// <summary>
|
||||
/// The m23 component.
|
||||
/// </summary>
|
||||
public double m5;
|
||||
/// <summary>
|
||||
/// The m24 component.
|
||||
/// </summary>
|
||||
public double m6;
|
||||
/// <summary>
|
||||
/// The m33 component.
|
||||
/// </summary>
|
||||
public double m7;
|
||||
/// <summary>
|
||||
/// The m34 component.
|
||||
/// </summary>
|
||||
public double m8;
|
||||
/// <summary>
|
||||
/// The m44 component.
|
||||
/// </summary>
|
||||
public double m9;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the component value with a specific index.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
/// <returns>The value.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a symmetric matrix with a value in each component.
|
||||
/// </summary>
|
||||
/// <param name="c">The component value.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a symmetric matrix.
|
||||
/// </summary>
|
||||
/// <param name="m0">The m11 component.</param>
|
||||
/// <param name="m1">The m12 component.</param>
|
||||
/// <param name="m2">The m13 component.</param>
|
||||
/// <param name="m3">The m14 component.</param>
|
||||
/// <param name="m4">The m22 component.</param>
|
||||
/// <param name="m5">The m23 component.</param>
|
||||
/// <param name="m6">The m24 component.</param>
|
||||
/// <param name="m7">The m33 component.</param>
|
||||
/// <param name="m8">The m34 component.</param>
|
||||
/// <param name="m9">The m44 component.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a symmetric matrix from a plane.
|
||||
/// </summary>
|
||||
/// <param name="a">The plane x-component.</param>
|
||||
/// <param name="b">The plane y-component</param>
|
||||
/// <param name="c">The plane z-component</param>
|
||||
/// <param name="d">The plane w-component</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Adds two matrixes together.
|
||||
/// </summary>
|
||||
/// <param name="a">The left hand side.</param>
|
||||
/// <param name="b">The right hand side.</param>
|
||||
/// <returns>The resulting matrix.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Determinant(0, 1, 2, 1, 4, 5, 2, 5, 7)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinant(1, 2, 3, 4, 5, 6, 5, 7, 8)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinant(0, 2, 3, 1, 5, 6, 2, 7, 8)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinant(0, 1, 3, 1, 4, 6, 2, 5, 8)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Computes the determinant of this matrix.
|
||||
/// </summary>
|
||||
/// <param name="a11">The a11 index.</param>
|
||||
/// <param name="a12">The a12 index.</param>
|
||||
/// <param name="a13">The a13 index.</param>
|
||||
/// <param name="a21">The a21 index.</param>
|
||||
/// <param name="a22">The a22 index.</param>
|
||||
/// <param name="a23">The a23 index.</param>
|
||||
/// <param name="a31">The a31 index.</param>
|
||||
/// <param name="a32">The a32 index.</param>
|
||||
/// <param name="a33">The a33 index.</param>
|
||||
/// <returns>The determinant value.</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A single precision 2D vector.
|
||||
/// </summary>
|
||||
public struct Vector2 : IEquatable<Vector2>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector2 zero = new Vector2(0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const float Epsilon = 9.99999944E-11f;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public float x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public float y;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public float Magnitude
|
||||
{
|
||||
get { return (float)System.Math.Sqrt(x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public float MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector2 Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector2 result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector2(float value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public Vector2(float x, float y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator +(Vector2 a, Vector2 b)
|
||||
{
|
||||
return new Vector2(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator -(Vector2 a, Vector2 b)
|
||||
{
|
||||
return new Vector2(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator *(Vector2 a, float d)
|
||||
{
|
||||
return new Vector2(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator *(float d, Vector2 a)
|
||||
{
|
||||
return new Vector2(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator /(Vector2 a, float d)
|
||||
{
|
||||
return new Vector2(a.x / d, a.y / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator -(Vector2 a)
|
||||
{
|
||||
return new Vector2(-a.x, -a.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector2 lhs, Vector2 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector2 lhs, Vector2 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector2(Vector2d v)
|
||||
{
|
||||
return new Vector2((float)v.x, (float)v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector2(Vector2i v)
|
||||
{
|
||||
return new Vector2(v.x, v.y);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x and y components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public void Set(float x, float y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector2 scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
float mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector2 vector = (Vector2)other;
|
||||
return (x == vector.x && y == vector.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector2 other)
|
||||
{
|
||||
return (x == other.x && y == other.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static float Dot(ref Vector2 lhs, ref Vector2 rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector2 a, ref Vector2 b, out Vector2 result)
|
||||
{
|
||||
result = new Vector2(a.x * b.x, a.y * b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A double precision 2D vector.
|
||||
/// </summary>
|
||||
public struct Vector2d : IEquatable<Vector2d>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector2d zero = new Vector2d(0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const double Epsilon = double.Epsilon;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public double x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public double y;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public double Magnitude
|
||||
{
|
||||
get { return System.Math.Sqrt(x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public double MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector2d Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector2d result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector2d(double value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public Vector2d(double x, double y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator +(Vector2d a, Vector2d b)
|
||||
{
|
||||
return new Vector2d(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator -(Vector2d a, Vector2d b)
|
||||
{
|
||||
return new Vector2d(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator *(Vector2d a, double d)
|
||||
{
|
||||
return new Vector2d(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator *(double d, Vector2d a)
|
||||
{
|
||||
return new Vector2d(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator /(Vector2d a, double d)
|
||||
{
|
||||
return new Vector2d(a.x / d, a.y / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator -(Vector2d a)
|
||||
{
|
||||
return new Vector2d(-a.x, -a.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector2d lhs, Vector2d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector2d lhs, Vector2d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector2d(Vector2 v)
|
||||
{
|
||||
return new Vector2d(v.x, v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector2d(Vector2i v)
|
||||
{
|
||||
return new Vector2d(v.x, v.y);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x and y components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public void Set(double x, double y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector2d scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
double mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector2d))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector2d vector = (Vector2d)other;
|
||||
return (x == vector.x && y == vector.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector2d other)
|
||||
{
|
||||
return (x == other.x && y == other.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static double Dot(ref Vector2d lhs, ref Vector2d rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector2d a, ref Vector2d b, out Vector2d result)
|
||||
{
|
||||
result = new Vector2d(a.x * b.x, a.y * b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A 2D integer vector.
|
||||
/// </summary>
|
||||
public struct Vector2i : IEquatable<Vector2i>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector2i zero = new Vector2i(0, 0);
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public int x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public int y;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public int Magnitude
|
||||
{
|
||||
get { return (int)System.Math.Sqrt(x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public int MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector2i(int value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public Vector2i(int x, int y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator +(Vector2i a, Vector2i b)
|
||||
{
|
||||
return new Vector2i(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator -(Vector2i a, Vector2i b)
|
||||
{
|
||||
return new Vector2i(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator *(Vector2i a, int d)
|
||||
{
|
||||
return new Vector2i(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator *(int d, Vector2i a)
|
||||
{
|
||||
return new Vector2i(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator /(Vector2i a, int d)
|
||||
{
|
||||
return new Vector2i(a.x / d, a.y / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator -(Vector2i a)
|
||||
{
|
||||
return new Vector2i(-a.x, -a.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector2i lhs, Vector2i rhs)
|
||||
{
|
||||
return (lhs.x == rhs.x && lhs.y == rhs.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector2i lhs, Vector2i rhs)
|
||||
{
|
||||
return (lhs.x != rhs.x || lhs.y != rhs.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static explicit operator Vector2i(Vector2 v)
|
||||
{
|
||||
return new Vector2i((int)v.x, (int)v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector2i(Vector2d v)
|
||||
{
|
||||
return new Vector2i((int)v.x, (int)v.y);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x and y components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public void Set(int x, int y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector2i scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector2i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector2i vector = (Vector2i)other;
|
||||
return (x == vector.x && y == vector.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector2i other)
|
||||
{
|
||||
return (x == other.x && y == other.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(CultureInfo.InvariantCulture),
|
||||
y.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The integer format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A single precision 3D vector.
|
||||
/// </summary>
|
||||
public struct Vector3 : IEquatable<Vector3>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector3 zero = new Vector3(0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const float Epsilon = 9.99999944E-11f;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public float x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public float y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public float z;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public float Magnitude
|
||||
{
|
||||
get { return (float)System.Math.Sqrt(x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public float MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector3 Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector3 result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector3(float value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public Vector3(float x, float y, float z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector from a double precision vector.
|
||||
/// </summary>
|
||||
/// <param name="vector">The double precision vector.</param>
|
||||
public Vector3(Vector3d vector)
|
||||
{
|
||||
this.x = (float)vector.x;
|
||||
this.y = (float)vector.y;
|
||||
this.z = (float)vector.z;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator +(Vector3 a, Vector3 b)
|
||||
{
|
||||
return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator -(Vector3 a, Vector3 b)
|
||||
{
|
||||
return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator *(Vector3 a, float d)
|
||||
{
|
||||
return new Vector3(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator *(float d, Vector3 a)
|
||||
{
|
||||
return new Vector3(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator /(Vector3 a, float d)
|
||||
{
|
||||
return new Vector3(a.x / d, a.y / d, a.z / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator -(Vector3 a)
|
||||
{
|
||||
return new Vector3(-a.x, -a.y, -a.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector3 lhs, Vector3 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector3 lhs, Vector3 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector3(Vector3d v)
|
||||
{
|
||||
return new Vector3((float)v.x, (float)v.y, (float)v.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector3(Vector3i v)
|
||||
{
|
||||
return new Vector3(v.x, v.y, v.z);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public void Set(float x, float y, float z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector3 scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
float mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector3 other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static float Dot(ref Vector3 lhs, ref Vector3 rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cross Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the angle between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="from">The from vector.</param>
|
||||
/// <param name="to">The to vector.</param>
|
||||
/// <returns>The angle.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes both vectors and makes them orthogonal to each other.
|
||||
/// </summary>
|
||||
/// <param name="normal">The normal vector.</param>
|
||||
/// <param name="tangent">The tangent.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A double precision 3D vector.
|
||||
/// </summary>
|
||||
public struct Vector3d : IEquatable<Vector3d>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector3d zero = new Vector3d(0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const double Epsilon = double.Epsilon;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public double x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public double y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public double z;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public double Magnitude
|
||||
{
|
||||
get { return System.Math.Sqrt(x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public double MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector3d Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector3d result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector3d(double value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public Vector3d(double x, double y, double z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector from a single precision vector.
|
||||
/// </summary>
|
||||
/// <param name="vector">The single precision vector.</param>
|
||||
public Vector3d(Vector3 vector)
|
||||
{
|
||||
this.x = vector.x;
|
||||
this.y = vector.y;
|
||||
this.z = vector.z;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator +(Vector3d a, Vector3d b)
|
||||
{
|
||||
return new Vector3d(a.x + b.x, a.y + b.y, a.z + b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator -(Vector3d a, Vector3d b)
|
||||
{
|
||||
return new Vector3d(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator *(Vector3d a, double d)
|
||||
{
|
||||
return new Vector3d(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator *(double d, Vector3d a)
|
||||
{
|
||||
return new Vector3d(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator /(Vector3d a, double d)
|
||||
{
|
||||
return new Vector3d(a.x / d, a.y / d, a.z / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator -(Vector3d a)
|
||||
{
|
||||
return new Vector3d(-a.x, -a.y, -a.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector3d lhs, Vector3d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector3d lhs, Vector3d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector3d(Vector3 v)
|
||||
{
|
||||
return new Vector3d(v.x, v.y, v.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector3d(Vector3i v)
|
||||
{
|
||||
return new Vector3d(v.x, v.y, v.z);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public void Set(double x, double y, double z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector3d scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
double mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector3d other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static double Dot(ref Vector3d lhs, ref Vector3d rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cross Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the angle between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="from">The from vector.</param>
|
||||
/// <param name="to">The to vector.</param>
|
||||
/// <returns>The angle.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A 3D integer vector.
|
||||
/// </summary>
|
||||
public struct Vector3i : IEquatable<Vector3i>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector3i zero = new Vector3i(0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public int x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public int y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public int z;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public int Magnitude
|
||||
{
|
||||
get { return (int)System.Math.Sqrt(x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public int MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector3i(int value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public Vector3i(int x, int y, int z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator +(Vector3i a, Vector3i b)
|
||||
{
|
||||
return new Vector3i(a.x + b.x, a.y + b.y, a.z + b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator -(Vector3i a, Vector3i b)
|
||||
{
|
||||
return new Vector3i(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator *(Vector3i a, int d)
|
||||
{
|
||||
return new Vector3i(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator *(int d, Vector3i a)
|
||||
{
|
||||
return new Vector3i(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator /(Vector3i a, int d)
|
||||
{
|
||||
return new Vector3i(a.x / d, a.y / d, a.z / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator -(Vector3i a)
|
||||
{
|
||||
return new Vector3i(-a.x, -a.y, -a.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector3i lhs, Vector3i rhs)
|
||||
{
|
||||
return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector3i lhs, Vector3i rhs)
|
||||
{
|
||||
return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector3i(Vector3 v)
|
||||
{
|
||||
return new Vector3i((int)v.x, (int)v.y, (int)v.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public void Set(int x, int y, int z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector3i scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector3i other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1}, {2})",
|
||||
x.ToString(CultureInfo.InvariantCulture),
|
||||
y.ToString(CultureInfo.InvariantCulture),
|
||||
z.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The integer format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A single precision 4D vector.
|
||||
/// </summary>
|
||||
public struct Vector4 : IEquatable<Vector4>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector4 zero = new Vector4(0, 0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const float Epsilon = 9.99999944E-11f;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public float x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public float y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public float z;
|
||||
/// <summary>
|
||||
/// The w component.
|
||||
/// </summary>
|
||||
public float w;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public float Magnitude
|
||||
{
|
||||
get { return (float)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public float MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector4 Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector4 result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector4(float value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
this.w = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator *(Vector4 a, float d)
|
||||
{
|
||||
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator *(float d, Vector4 a)
|
||||
{
|
||||
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator /(Vector4 a, float d)
|
||||
{
|
||||
return new Vector4(a.x / d, a.y / d, a.z / d, a.w / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator -(Vector4 a)
|
||||
{
|
||||
return new Vector4(-a.x, -a.y, -a.z, -a.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector4 lhs, Vector4 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector4 lhs, Vector4 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector4(Vector4d v)
|
||||
{
|
||||
return new Vector4((float)v.x, (float)v.y, (float)v.z, (float)v.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector4(Vector4i v)
|
||||
{
|
||||
return new Vector4(v.x, v.y, v.z, v.w);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public void Set(float x, float y, float z, float w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector4 scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
w *= scale.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
float mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
w /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector4 other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A double precision 4D vector.
|
||||
/// </summary>
|
||||
public struct Vector4d : IEquatable<Vector4d>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector4d zero = new Vector4d(0, 0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const double Epsilon = double.Epsilon;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public double x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public double y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public double z;
|
||||
/// <summary>
|
||||
/// The w component.
|
||||
/// </summary>
|
||||
public double w;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public double Magnitude
|
||||
{
|
||||
get { return System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public double MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector4d Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector4d result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector4d(double value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
this.w = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator *(Vector4d a, double d)
|
||||
{
|
||||
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator *(double d, Vector4d a)
|
||||
{
|
||||
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator /(Vector4d a, double d)
|
||||
{
|
||||
return new Vector4d(a.x / d, a.y / d, a.z / d, a.w / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator -(Vector4d a)
|
||||
{
|
||||
return new Vector4d(-a.x, -a.y, -a.z, -a.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector4d lhs, Vector4d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector4d lhs, Vector4d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector4d(Vector4 v)
|
||||
{
|
||||
return new Vector4d(v.x, v.y, v.z, v.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector4d(Vector4i v)
|
||||
{
|
||||
return new Vector4d(v.x, v.y, v.z, v.w);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public void Set(double x, double y, double z, double w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector4d scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
w *= scale.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
double mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
w /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector4d other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A 4D integer vector.
|
||||
/// </summary>
|
||||
public struct Vector4i : IEquatable<Vector4i>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector4i zero = new Vector4i(0, 0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public int x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public int y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public int z;
|
||||
/// <summary>
|
||||
/// The w component.
|
||||
/// </summary>
|
||||
public int w;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public int Magnitude
|
||||
{
|
||||
get { return (int)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public int MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector4i(int value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
this.w = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator *(Vector4i a, int d)
|
||||
{
|
||||
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator *(int d, Vector4i a)
|
||||
{
|
||||
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator /(Vector4i a, int d)
|
||||
{
|
||||
return new Vector4i(a.x / d, a.y / d, a.z / d, a.w / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator -(Vector4i a)
|
||||
{
|
||||
return new Vector4i(-a.x, -a.y, -a.z, -a.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static explicit operator Vector4i(Vector4 v)
|
||||
{
|
||||
return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public void Set(int x, int y, int z, int w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector4i scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
w *= scale.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector4i other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The integer format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
955
LightlessSync/ThirdParty/MeshDecimator/Mesh.cs
vendored
955
LightlessSync/ThirdParty/MeshDecimator/Mesh.cs
vendored
@@ -1,955 +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
|
||||
{
|
||||
/// <summary>
|
||||
/// A mesh.
|
||||
/// </summary>
|
||||
public sealed class Mesh
|
||||
{
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The count of supported UV channels.
|
||||
/// </summary>
|
||||
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 Vector2[][] uvs2D = null;
|
||||
private Vector3[][] uvs3D = null;
|
||||
private Vector4[][] uvs4D = null;
|
||||
private Vector4[] colors = null;
|
||||
private BoneWeight[] boneWeights = null;
|
||||
|
||||
private static readonly int[] emptyIndices = new int[0];
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the count of vertices of this mesh.
|
||||
/// </summary>
|
||||
public int VertexCount
|
||||
{
|
||||
get { return vertices.Length; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the count of submeshes in this mesh.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total count of triangles in this mesh.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertices for this mesh. Note that this resets all other vertex attributes.
|
||||
/// </summary>
|
||||
public Vector3d[] Vertices
|
||||
{
|
||||
get { return vertices; }
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException("value");
|
||||
|
||||
vertices = value;
|
||||
ClearVertexAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the combined indices for this mesh. Once set, the sub-mesh count gets set to 1.
|
||||
/// </summary>
|
||||
public int[] Indices
|
||||
{
|
||||
get
|
||||
{
|
||||
if (indices.Length == 1)
|
||||
{
|
||||
return indices[0] ?? emptyIndices;
|
||||
}
|
||||
else
|
||||
{
|
||||
List<int> indexList = new List<int>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the normals for this mesh.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tangents for this mesh.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the first UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV1
|
||||
{
|
||||
get { return GetUVs2D(0); }
|
||||
set { SetUVs(0, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the second UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV2
|
||||
{
|
||||
get { return GetUVs2D(1); }
|
||||
set { SetUVs(1, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the third UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV3
|
||||
{
|
||||
get { return GetUVs2D(2); }
|
||||
set { SetUVs(2, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fourth UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV4
|
||||
{
|
||||
get { return GetUVs2D(3); }
|
||||
set { SetUVs(3, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertex colors for this mesh.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertex bone weights for this mesh.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Creates a new mesh.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The mesh vertices.</param>
|
||||
/// <param name="indices">The mesh indices.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new mesh.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The mesh vertices.</param>
|
||||
/// <param name="indices">The mesh indices.</param>
|
||||
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;
|
||||
uvs2D = null;
|
||||
uvs3D = null;
|
||||
uvs4D = null;
|
||||
colors = null;
|
||||
boneWeights = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Recalculate Normals
|
||||
/// <summary>
|
||||
/// Recalculates the normals for this mesh smoothly.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Recalculates the tangents for this mesh.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns the count of triangles for a specific sub-mesh in this mesh.
|
||||
/// </summary>
|
||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
||||
/// <returns>The triangle count.</returns>
|
||||
public int GetTriangleCount(int subMeshIndex)
|
||||
{
|
||||
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return indices[subMeshIndex].Length / 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the triangle indices of a specific sub-mesh in this mesh.
|
||||
/// </summary>
|
||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
||||
/// <returns>The triangle indices.</returns>
|
||||
public int[] GetIndices(int subMeshIndex)
|
||||
{
|
||||
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return indices[subMeshIndex] ?? emptyIndices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the triangle indices for all sub-meshes in this mesh.
|
||||
/// </summary>
|
||||
/// <returns>The sub-mesh triangle indices.</returns>
|
||||
public int[][] GetSubMeshIndices()
|
||||
{
|
||||
var subMeshIndices = new int[indices.Length][];
|
||||
for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++)
|
||||
{
|
||||
subMeshIndices[subMeshIndex] = indices[subMeshIndex] ?? emptyIndices;
|
||||
}
|
||||
return subMeshIndices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the triangle indices of a specific sub-mesh in this mesh.
|
||||
/// </summary>
|
||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
||||
/// <param name="indices">The triangle indices.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Returns the UV dimension for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <returns>The UV dimension count.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (2D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <returns>The UVs.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (3D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <returns>The UVs.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (4D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <returns>The UVs.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (2D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void GetUVs(int channel, List<Vector2> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (3D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void GetUVs(int channel, List<Vector3> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (4D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void GetUVs(int channel, List<Vector4> 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
|
||||
/// <summary>
|
||||
/// Sets the UVs (2D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (3D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (4D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (2D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, List<Vector2> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (3D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, List<Vector3> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (4D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, List<Vector4> 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
|
||||
/// <summary>
|
||||
/// Returns the text-representation of this mesh.
|
||||
/// </summary>
|
||||
/// <returns>The text-representation.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Vertices: {0}", vertices.Length);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// The decimation algorithms.
|
||||
/// </summary>
|
||||
public enum Algorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// The default algorithm.
|
||||
/// </summary>
|
||||
Default,
|
||||
/// <summary>
|
||||
/// The fast quadric mesh simplification algorithm.
|
||||
/// </summary>
|
||||
FastQuadricMesh
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The mesh decimation API.
|
||||
/// </summary>
|
||||
public static class MeshDecimation
|
||||
{
|
||||
#region Public Methods
|
||||
#region Create Algorithm
|
||||
/// <summary>
|
||||
/// Creates a specific decimation algorithm.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The desired algorithm.</param>
|
||||
/// <returns>The decimation algorithm.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Decimates a mesh.
|
||||
/// </summary>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMesh(Mesh mesh, int targetTriangleCount)
|
||||
{
|
||||
return DecimateMesh(Algorithm.Default, mesh, targetTriangleCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The desired algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The decimation algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
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
|
||||
/// <summary>
|
||||
/// Decimates a mesh without losing any quality.
|
||||
/// </summary>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMeshLossless(Mesh mesh)
|
||||
{
|
||||
return DecimateMeshLossless(Algorithm.Default, mesh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh without losing any quality.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The desired algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMeshLossless(Algorithm algorithm, Mesh mesh)
|
||||
{
|
||||
if (mesh == null)
|
||||
throw new ArgumentNullException("mesh");
|
||||
|
||||
var decimationAlgorithm = CreateAlgorithm(algorithm);
|
||||
return DecimateMeshLossless(decimationAlgorithm, mesh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh without losing any quality.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The decimation algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
1325
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/Decimate.cs
vendored
Normal file
1325
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/Decimate.cs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
88
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeCollapse.cs
vendored
Normal file
88
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeCollapse.cs
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public partial class DecimateModifier
|
||||
{
|
||||
public class EdgeCollapse : IComparable<EdgeCollapse>, IEquatable<EdgeCollapse>
|
||||
{
|
||||
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 $"<A:{posA} B:{posB} error:{error} topology:{_weight}>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeComparer.cs
vendored
Normal file
15
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeComparer.cs
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public partial class DecimateModifier
|
||||
{
|
||||
private class EdgeComparer : IComparer<EdgeCollapse>
|
||||
{
|
||||
public int Compare(EdgeCollapse x, EdgeCollapse y)
|
||||
{
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/SceneDecimator.cs
vendored
Normal file
72
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/SceneDecimator.cs
vendored
Normal file
@@ -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<ConnectedMesh, ModifierAndOccurrences> _modifiers;
|
||||
|
||||
public void Initialize(IEnumerable<ConnectedMesh> meshes)
|
||||
{
|
||||
_modifiers = new Dictionary<ConnectedMesh, ModifierAndOccurrences>();
|
||||
|
||||
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<ConnectedMesh, ModifierAndOccurrences> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsCreator.cs
vendored
Normal file
76
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsCreator.cs
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public class NormalsModifier
|
||||
{
|
||||
public struct PosAndAttribute : IEquatable<PosAndAttribute>
|
||||
{
|
||||
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<PosAndAttribute, int> attributeToIndex = new Dictionary<PosAndAttribute, int>();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
17
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsFixer.cs
vendored
Normal file
17
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsFixer.cs
vendored
Normal file
@@ -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;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
27
LightlessSync/ThirdParty/Nanomesh/Algo/Triangulate.cs
vendored
Normal file
27
LightlessSync/ThirdParty/Nanomesh/Algo/Triangulate.cs
vendored
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
144
LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs
vendored
Normal file
144
LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public readonly struct BoneWeight : IEquatable<BoneWeight>, IInterpolable<BoneWeight>
|
||||
{
|
||||
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<int, float> newBoneWeight = new Dictionary<int, float>();
|
||||
|
||||
// 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<int, float> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
110
LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs
vendored
Normal file
110
LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public readonly struct Color32 : IEquatable<Color32>, IInterpolable<Color32>
|
||||
{
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds two colors.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts one color from another.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies one color by another.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
/// <summary>
|
||||
/// Divides one color over another.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies a color by a number.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="d"></param>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies a color by a number.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
/// <summary>
|
||||
/// Divides a color by a number.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
}
|
||||
}
|
||||
347
LightlessSync/ThirdParty/Nanomesh/Base/FfxivVertexAttribute.cs
vendored
Normal file
347
LightlessSync/ThirdParty/Nanomesh/Base/FfxivVertexAttribute.cs
vendored
Normal file
@@ -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<FfxivVertexAttribute>, IInterpolable<FfxivVertexAttribute>
|
||||
{
|
||||
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<int> indices = stackalloc int[8];
|
||||
Span<float> weights = stackalloc float[8];
|
||||
var count = 0;
|
||||
|
||||
static void AddWeight(Span<int> indices, Span<float> 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<int> topIndices = stackalloc int[4];
|
||||
Span<float> 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
LightlessSync/ThirdParty/Nanomesh/Base/IInterpolable.cs
vendored
Normal file
7
LightlessSync/ThirdParty/Nanomesh/Base/IInterpolable.cs
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Nanomesh
|
||||
{
|
||||
public interface IInterpolable<T>
|
||||
{
|
||||
T Interpolate(T other, double ratio);
|
||||
}
|
||||
}
|
||||
356
LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs
vendored
Normal file
356
LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs
vendored
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs
vendored
Normal file
114
LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs
vendored
Normal file
@@ -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<T>(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<T>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
LightlessSync/ThirdParty/Nanomesh/Base/Profiling.cs
vendored
Normal file
50
LightlessSync/ThirdParty/Nanomesh/Base/Profiling.cs
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public static class Profiling
|
||||
{
|
||||
private static readonly Dictionary<string, Stopwatch> stopwatches = new Dictionary<string, Stopwatch>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
632
LightlessSync/ThirdParty/Nanomesh/Base/Quaternion.cs
vendored
Normal file
632
LightlessSync/ThirdParty/Nanomesh/Base/Quaternion.cs
vendored
Normal file
@@ -0,0 +1,632 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public partial struct Quaternion : IEquatable<Quaternion>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// <para>The identity rotation (RO).</para>
|
||||
/// </summary>
|
||||
public static Quaternion identity => new Quaternion(0, 0, 0, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length (magnitude) of the quaternion.
|
||||
/// </summary>
|
||||
/// <seealso cref="LengthSquared"/>
|
||||
public double Length => (double)System.Math.Sqrt(x * x + y * y + z * z + w * w);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the square of the quaternion length (magnitude).
|
||||
/// </summary>
|
||||
public double LengthSquared => x * x + y * y + z * z + w * w;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Constructs new Quaternion with given x,y,z,w components.</para>
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <param name="z"></param>
|
||||
/// <param name="w"></param>
|
||||
public Quaternion(double x, double y, double z, double w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new Quaternion from vector and w components
|
||||
/// </summary>
|
||||
/// <param name="v">The vector part</param>
|
||||
/// <param name="w">The w part</param>
|
||||
public Quaternion(Vector3 v, double w)
|
||||
{
|
||||
x = v.x;
|
||||
y = v.y;
|
||||
z = v.z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Set x, y, z and w components of an existing Quaternion.</para>
|
||||
/// </summary>
|
||||
/// <param name="new_x"></param>
|
||||
/// <param name="new_y"></param>
|
||||
/// <param name="new_z"></param>
|
||||
/// <param name="new_w"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the Quaternion to unit length.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scale the given quaternion to unit length
|
||||
/// </summary>
|
||||
/// <param name="q">The quaternion to normalize</param>
|
||||
/// <param name="result">The normalized quaternion</param>
|
||||
public void Normalize()
|
||||
{
|
||||
this = Normalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>The dot product between two rotations.</para>
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Creates a rotation which rotates /angle/ degrees around /axis/.</para>
|
||||
/// </summary>
|
||||
/// <param name="angle"></param>
|
||||
/// <param name="axis"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Creates a rotation which rotates from /fromDirection/ to /toDirection/.</para>
|
||||
/// </summary>
|
||||
/// <param name="fromDirection"></param>
|
||||
/// <param name="toDirection"></param>
|
||||
public static Quaternion FromToRotation(Vector3 fromDirection, Vector3 toDirection)
|
||||
{
|
||||
return RotateTowards(LookRotation(fromDirection), LookRotation(toDirection), double.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Creates a rotation which rotates from /fromDirection/ to /toDirection/.</para>
|
||||
/// </summary>
|
||||
/// <param name="fromDirection"></param>
|
||||
/// <param name="toDirection"></param>
|
||||
public void SetFromToRotation(Vector3 fromDirection, Vector3 toDirection)
|
||||
{
|
||||
this = Quaternion.FromToRotation(fromDirection, toDirection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Creates a rotation with the specified /forward/ and /upwards/ directions.</para>
|
||||
/// </summary>
|
||||
/// <param name="forward">The direction to look in.</param>
|
||||
/// <param name="upwards">The vector that defines in which direction up is.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Creates a rotation with the specified /forward/ and /upwards/ directions.</para>
|
||||
/// </summary>
|
||||
/// <param name="view">The direction to look in.</param>
|
||||
/// <param name="up">The vector that defines in which direction up is.</param>
|
||||
public void SetLookRotation(Vector3 view, Vector3 up)
|
||||
{
|
||||
this = Quaternion.LookRotation(view, up);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Spherically interpolates between /a/ and /b/ by t. The parameter /t/ is clamped to the range [0, 1].</para>
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <param name="t"></param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Spherically interpolates between /a/ and /b/ by t. The parameter /t/ is not clamped.</para>
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <param name="t"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Interpolates between /a/ and /b/ by /t/ and normalizes the result afterwards. The parameter /t/ is clamped to the range [0, 1].</para>
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <param name="t"></param>
|
||||
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" ???
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Interpolates between /a/ and /b/ by /t/ and normalizes the result afterwards. The parameter /t/ is not clamped.</para>
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <param name="t"></param>
|
||||
public static Quaternion LerpUnclamped(Quaternion a, Quaternion b, double t)
|
||||
{
|
||||
return Slerp(ref a, ref b, t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Rotates a rotation /from/ towards /to/.</para>
|
||||
/// </summary>
|
||||
/// <param name="from"></param>
|
||||
/// <param name="to"></param>
|
||||
/// <param name="maxDegreesDelta"></param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Returns the Inverse of /rotation/.</para>
|
||||
/// </summary>
|
||||
/// <param name="rotation"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Returns a nicely formatted string of the Quaternion.</para>
|
||||
/// </summary>
|
||||
/// <param name="format"></param>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{x}, {y}, {z}, {w}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Returns a nicely formatted string of the Quaternion.</para>
|
||||
/// </summary>
|
||||
/// <param name="format"></param>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1}, {2}, {3})", x.ToString(format), y.ToString(format), z.ToString(format), w.ToString(format));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Returns the angle in degrees between two rotations /a/ and /b/.</para>
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>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).</para>
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <param name="z"></param>
|
||||
public static Quaternion Euler(double x, double y, double z)
|
||||
{
|
||||
return Quaternion.FromEulerRad(new Vector3((double)x, (double)y, (double)z) * degToRad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>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).</para>
|
||||
/// </summary>
|
||||
/// <param name="euler"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
LightlessSync/ThirdParty/Nanomesh/Base/SymmetricMatrix.cs
vendored
Normal file
97
LightlessSync/ThirdParty/Nanomesh/Base/SymmetricMatrix.cs
vendored
Normal file
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
26
LightlessSync/ThirdParty/Nanomesh/Base/TextUtils.cs
vendored
Normal file
26
LightlessSync/ThirdParty/Nanomesh/Base/TextUtils.cs
vendored
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
377
LightlessSync/ThirdParty/Nanomesh/Base/Vector2.cs
vendored
Normal file
377
LightlessSync/ThirdParty/Nanomesh/Base/Vector2.cs
vendored
Normal file
@@ -0,0 +1,377 @@
|
||||
using System;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public readonly struct Vector2 : IEquatable<Vector2>, IInterpolable<Vector2>
|
||||
{
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the squared length of this vector (RO).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
public static double Dot(Vector2 lhs, Vector2 rhs) { return lhs.x * rhs.x + lhs.y * rhs.y; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the angle in radians between /from/ and /to/.
|
||||
/// </summary>
|
||||
/// <param name="from"></param>
|
||||
/// <param name="to"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the signed angle in degrees between /from/ and /to/. Always returns the smallest possible angle
|
||||
/// </summary>
|
||||
/// <param name="from"></param>
|
||||
/// <param name="to"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the distance between /a/ and /b/.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of /vector/ with its magnitude clamped to /maxLength/.
|
||||
/// </summary>
|
||||
/// <param name="vector"></param>
|
||||
/// <param name="maxLength"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a vector that is made from the smallest components of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 Min(Vector2 lhs, Vector2 rhs) { return new Vector2(Math.Min(lhs.x, rhs.x), Math.Min(lhs.y, rhs.y)); }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a vector that is made from the largest components of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator +(Vector2 a, Vector2 b) { return new Vector2(a.x + b.x, a.y + b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts one vector from another.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator -(Vector2 a, Vector2 b) { return new Vector2(a.x - b.x, a.y - b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies one vector by another.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator *(Vector2 a, Vector2 b) { return new Vector2(a.x * b.x, a.y * b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Divides one vector over another.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator /(Vector2 a, Vector2 b) { return new Vector2(a.x / b.x, a.y / b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Negates a vector.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator -(Vector2 a) { return new Vector2(-a.x, -a.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies a vector by a number.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="d"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator *(Vector2 a, double d) { return new Vector2(a.x * d, a.y * d); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies a vector by a number.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <param name="a"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator *(double d, Vector2 a) { return new Vector2(a.x * d, a.y * d); }
|
||||
|
||||
/// <summary>
|
||||
/// Divides a vector by a number.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="d"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2 operator /(Vector2 a, double d) { return new Vector2(a.x / d, a.y / d); }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the vectors are equal.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if vectors are different.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
public static bool operator !=(Vector2 lhs, Vector2 rhs)
|
||||
{
|
||||
// Returns true in the presence of NaN values.
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a [[Vector3]] to a Vector2.
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
public static implicit operator Vector2(Vector3F v)
|
||||
{
|
||||
return new Vector2(v.x, v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Vector2 to a [[Vector3]].
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
371
LightlessSync/ThirdParty/Nanomesh/Base/Vector2F.cs
vendored
Normal file
371
LightlessSync/ThirdParty/Nanomesh/Base/Vector2F.cs
vendored
Normal file
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public readonly struct Vector2F : IEquatable<Vector2F>, IInterpolable<Vector2F>
|
||||
{
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the squared length of this vector (RO).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
public static float Dot(Vector2F lhs, Vector2F rhs) { return lhs.x * rhs.x + lhs.y * rhs.y; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the angle in radians between /from/ and /to/.
|
||||
/// </summary>
|
||||
/// <param name="from"></param>
|
||||
/// <param name="to"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the signed angle in degrees between /from/ and /to/. Always returns the smallest possible angle
|
||||
/// </summary>
|
||||
/// <param name="from"></param>
|
||||
/// <param name="to"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the distance between /a/ and /b/.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of /vector/ with its magnitude clamped to /maxLength/.
|
||||
/// </summary>
|
||||
/// <param name="vector"></param>
|
||||
/// <param name="maxLength"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a vector that is made from the smallest components of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2F Min(Vector2F lhs, Vector2F rhs) { return new Vector2F(MathF.Min(lhs.x, rhs.x), MathF.Min(lhs.y, rhs.y)); }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a vector that is made from the largest components of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2F operator +(Vector2F a, Vector2F b) { return new Vector2F(a.x + b.x, a.y + b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts one vector from another.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2F operator -(Vector2F a, Vector2F b) { return new Vector2F(a.x - b.x, a.y - b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies one vector by another.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2F operator *(Vector2F a, Vector2F b) { return new Vector2F(a.x * b.x, a.y * b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Divides one vector over another.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2F operator /(Vector2F a, Vector2F b) { return new Vector2F(a.x / b.x, a.y / b.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Negates a vector.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2F operator -(Vector2F a) { return new Vector2F(-a.x, -a.y); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies a vector by a number.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="d"></param>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies a vector by a number.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <param name="a"></param>
|
||||
/// <returns></returns>
|
||||
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); }
|
||||
|
||||
/// <summary>
|
||||
/// Divides a vector by a number.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <param name="d"></param>
|
||||
/// <returns></returns>
|
||||
public static Vector2F operator /(Vector2F a, float d) { return new Vector2F(a.x / d, a.y / d); }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the vectors are equal.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if vectors are different.
|
||||
/// </summary>
|
||||
/// <param name="lhs"></param>
|
||||
/// <param name="rhs"></param>
|
||||
/// <returns></returns>
|
||||
public static bool operator !=(Vector2F lhs, Vector2F rhs)
|
||||
{
|
||||
// Returns true in the presence of NaN values.
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a [[Vector3]] to a Vector2.
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
public static implicit operator Vector2F(Vector3F v)
|
||||
{
|
||||
return new Vector2F(v.x, v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Vector2 to a [[Vector3]].
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
28
LightlessSync/ThirdParty/Nanomesh/Base/Vector2FComparer.cs
vendored
Normal file
28
LightlessSync/ThirdParty/Nanomesh/Base/Vector2FComparer.cs
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public class Vector2FComparer : IEqualityComparer<Vector2F>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
191
LightlessSync/ThirdParty/Nanomesh/Base/Vector3.cs
vendored
Normal file
191
LightlessSync/ThirdParty/Nanomesh/Base/Vector3.cs
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public readonly struct Vector3 : IEquatable<Vector3>, IInterpolable<Vector3>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
26
LightlessSync/ThirdParty/Nanomesh/Base/Vector3Comparer.cs
vendored
Normal file
26
LightlessSync/ThirdParty/Nanomesh/Base/Vector3Comparer.cs
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public class Vector3Comparer : IEqualityComparer<Vector3>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
LightlessSync/ThirdParty/Nanomesh/Base/Vector3F.cs
vendored
Normal file
172
LightlessSync/ThirdParty/Nanomesh/Base/Vector3F.cs
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
using System;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public readonly struct Vector3F : IEquatable<Vector3F>, IInterpolable<Vector3F>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
LightlessSync/ThirdParty/Nanomesh/Base/Vector3FComparer.cs
vendored
Normal file
29
LightlessSync/ThirdParty/Nanomesh/Base/Vector3FComparer.cs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public class Vector3FComparer : IEqualityComparer<Vector3F>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
LightlessSync/ThirdParty/Nanomesh/Base/Vector4F.cs
vendored
Normal file
91
LightlessSync/ThirdParty/Nanomesh/Base/Vector4F.cs
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
|
||||
namespace Nanomesh
|
||||
{
|
||||
public readonly struct Vector4F : IEquatable<Vector4F>, IInterpolable<Vector4F>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user