From da802e8e89938b77430e283f64890dd1f952b5eb Mon Sep 17 00:00:00 2001 From: Mengzhu Wang Date: Sun, 11 Jan 2026 17:25:52 +0100 Subject: [PATCH 1/3] add referenceManager --- SeeSharp.Blazor/LongSetting.razor | 25 + SeeSharp.ReferenceManager/App.razor | 12 + SeeSharp.ReferenceManager/Imports.cs | 32 + SeeSharp.ReferenceManager/MainLayout.razor | 3 + .../Pages/DocumentationHelper.cs | 71 ++ SeeSharp.ReferenceManager/Pages/Index.razor | 39 + .../Pages/IntegratorSelector.razor | 243 +++++ .../Pages/IntegratorSelector.razor.css | 104 +++ .../Pages/ReferenceRendering.razor | 838 ++++++++++++++++++ .../Pages/ReferenceRendering.razor.css | 159 ++++ SeeSharp.ReferenceManager/Pages/_Host.cshtml | 34 + SeeSharp.ReferenceManager/Program.cs | 27 + .../SeeSharp.ReferenceManager.csproj | 17 + SeeSharp.ReferenceManager/_Imports.razor | 4 + .../appsettings.Development.json | 9 + SeeSharp.ReferenceManager/appsettings.json | 9 + .../wwwroot/css/site.css | 86 ++ 17 files changed, 1712 insertions(+) create mode 100644 SeeSharp.Blazor/LongSetting.razor create mode 100644 SeeSharp.ReferenceManager/App.razor create mode 100644 SeeSharp.ReferenceManager/Imports.cs create mode 100644 SeeSharp.ReferenceManager/MainLayout.razor create mode 100644 SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs create mode 100644 SeeSharp.ReferenceManager/Pages/Index.razor create mode 100644 SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor create mode 100644 SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css create mode 100644 SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor create mode 100644 SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css create mode 100644 SeeSharp.ReferenceManager/Pages/_Host.cshtml create mode 100644 SeeSharp.ReferenceManager/Program.cs create mode 100644 SeeSharp.ReferenceManager/SeeSharp.ReferenceManager.csproj create mode 100644 SeeSharp.ReferenceManager/_Imports.razor create mode 100644 SeeSharp.ReferenceManager/appsettings.Development.json create mode 100644 SeeSharp.ReferenceManager/appsettings.json create mode 100644 SeeSharp.ReferenceManager/wwwroot/css/site.css diff --git a/SeeSharp.Blazor/LongSetting.razor b/SeeSharp.Blazor/LongSetting.razor new file mode 100644 index 00000000..fab53adb --- /dev/null +++ b/SeeSharp.Blazor/LongSetting.razor @@ -0,0 +1,25 @@ +@using Microsoft.AspNetCore.Mvc + +@namespace SeeSharp.Blazor + +@inherits SettingBase + +@{ + base.BuildRenderTree(__builder); +} + +@code { + protected override string Type => "number"; + + protected override long ParseValue(ChangeEventArgs e) + { + if (long.TryParse((string)e.Value, out long result)) + return result; + return Value; + } + + protected override Dictionary CustomAttributes => new() { + { "step", 1 } + }; +} + diff --git a/SeeSharp.ReferenceManager/App.razor b/SeeSharp.ReferenceManager/App.razor new file mode 100644 index 00000000..6fd3ed1b --- /dev/null +++ b/SeeSharp.ReferenceManager/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/SeeSharp.ReferenceManager/Imports.cs b/SeeSharp.ReferenceManager/Imports.cs new file mode 100644 index 00000000..b6c33e5a --- /dev/null +++ b/SeeSharp.ReferenceManager/Imports.cs @@ -0,0 +1,32 @@ +global using System; +global using System.Collections.Concurrent; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.IO; +global using System.Linq; +global using System.Numerics; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Threading; +global using System.Threading.Tasks; + +global using TinyEmbree; +global using SimpleImageIO; + +global using SeeSharp; +global using SeeSharp.Cameras; +global using SeeSharp.Common; +global using SeeSharp.Experiments; +global using SeeSharp.Geometry; +global using SeeSharp.Images; +global using SeeSharp.Integrators; +global using SeeSharp.Integrators.Bidir; +global using SeeSharp.Integrators.Common; +global using SeeSharp.Integrators.Util; +global using SeeSharp.Sampling; +global using SeeSharp.Shading; +global using SeeSharp.Shading.Background; +global using SeeSharp.Shading.Emitters; +global using SeeSharp.Shading.Materials; + +global using SeeSharp.Blazor; diff --git a/SeeSharp.ReferenceManager/MainLayout.razor b/SeeSharp.ReferenceManager/MainLayout.razor new file mode 100644 index 00000000..a5af3489 --- /dev/null +++ b/SeeSharp.ReferenceManager/MainLayout.razor @@ -0,0 +1,3 @@ +@inherits LayoutComponentBase + +
@Body
diff --git a/SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs b/SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs new file mode 100644 index 00000000..ed2449bd --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace SeeSharp.Blazor; + +public static class DocumentationHelper +{ + private static Dictionary _loadedXmlDocumentation = new(); + + public static void LoadXmlDocumentation(Assembly assembly) + { + var assemblyPath = assembly.Location; + var xmlPath = Path.ChangeExtension(assemblyPath, ".xml"); + + if (File.Exists(xmlPath)) + { + try + { + var doc = XDocument.Load(xmlPath); + foreach (var member in doc.Descendants("member")) + { + var name = member.Attribute("name")?.Value; + var summary = member.Element("summary")?.Value.Trim(); + + if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(summary)) + { + string cleanSummary = Regex.Replace(summary, @"\s+", " "); + _loadedXmlDocumentation[name] = cleanSummary; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[DocHelper] Error loading XML: {ex.Message}"); + } + } + } + + public static string GetSummary(MemberInfo member) + { + if (member == null || member.DeclaringType == null) return ""; + + string prefix = member is PropertyInfo ? "P:" : "F:"; + + Type declaringType = member.DeclaringType; + string typeName = declaringType.FullName; + + if (declaringType.IsGenericType) + { + int bracketIndex = typeName.IndexOf('['); + if (bracketIndex > 0) + { + typeName = typeName.Substring(0, bracketIndex); + } + } + + string key = $"{prefix}{typeName}.{member.Name}"; + + if (_loadedXmlDocumentation.TryGetValue(key, out var summary)) + { + return summary; + } + + return ""; + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/Index.razor b/SeeSharp.ReferenceManager/Pages/Index.razor new file mode 100644 index 00000000..60c1eed3 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/Index.razor @@ -0,0 +1,39 @@ +@page "/" + +@using System.Reflection +@using System.Text.RegularExpressions + + +
+ +
+ + +@code { + /// Enumerates all .razor components in this folder + public IEnumerable<(string Name, string Url)> GetExperimentPages() + { + var routableComponents = Assembly + .GetExecutingAssembly() + .ExportedTypes + .Where(t => t.IsSubclassOf(typeof(ComponentBase))) + .Where(c => c + .GetCustomAttributes(inherit: true) + .OfType() + .Count() > 0); + + foreach (var routableComponent in routableComponents) + { + string name = routableComponent.ToString().Replace("SeeSharp.ReferenceManager.Pages.", string.Empty); + if (name != "Index") + yield return (name, name); + } + } +} diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor new file mode 100644 index 00000000..52b77f29 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor @@ -0,0 +1,243 @@ +@using SeeSharp.Experiments +@using SeeSharp +@using SeeSharp.Blazor +@using SeeSharp.Integrators +@using System.Reflection +@using System.Collections.Generic +@using System.ComponentModel + +
+
+ + +
+
+ +
+ @if (CurrentIntegrator != null) + { + var groups = GetParameterGroups(CurrentIntegrator); + + @foreach (var group in groups) + { +
+ + @group.Title + + +
+ @foreach (var prop in group.Properties) + { + @RenderSetting(prop, () => prop.GetValue(CurrentIntegrator), v => prop.SetValue(CurrentIntegrator, v)) + } + @foreach (var field in group.Fields) + { + @RenderSetting(field, () => field.GetValue(CurrentIntegrator), v => field.SetValue(CurrentIntegrator, v)) + } +
+
+ } + } +
+ +@code { + [Parameter] public Scene scene { get; set; } = default!; + + public List addedIntegrators { get; private set; } = new(); + + public Integrator? CurrentIntegrator => addedIntegrators.FirstOrDefault(); + + Type[] integratorTypes = Array.Empty(); + string? selectedIntegrator; + + class ParameterGroup + { + public string Title { get; set; } = ""; + public List Properties { get; set; } = new(); + public List Fields { get; set; } = new(); + public bool HasParameters => Properties.Any() || Fields.Any(); + } + + protected override void OnInitialized() + { + integratorTypes = new Type[] {typeof(PathTracer), typeof(VertexConnectionAndMerging)}; + if (integratorTypes.Length > 0) { + selectedIntegrator = integratorTypes[0].FullName; + ReplaceIntegrator(); + } + DocumentationHelper.LoadXmlDocumentation(typeof(Integrator).Assembly); + } + + void OnSelectionChanged(ChangeEventArgs e) + { + selectedIntegrator = e.Value?.ToString(); + if (!string.IsNullOrEmpty(selectedIntegrator)) + { + ReplaceIntegrator(); + } + } + + void ReplaceIntegrator() + { + if (string.IsNullOrEmpty(selectedIntegrator)) return; + var type = integratorTypes.FirstOrDefault(t => t.FullName == selectedIntegrator); + if (type == null) return; + + addedIntegrators.Clear(); + + var integrator = (Integrator)Activator.CreateInstance(type)!; + addedIntegrators.Add(integrator); + + StateHasChanged(); + } + + List GetParameterGroups(Integrator integrator) + { + var groups = new List(); + var currentType = integrator.GetType(); + + var allProps = GetFilteredProps(currentType); + var allFields = GetFilteredFields(currentType); + + while (currentType != null && currentType != typeof(object)) { + bool IsCurrentDeclared(MemberInfo m) + { + var d = m.DeclaringType; + var cur = currentType; + if (d != null && d.IsGenericType && !d.IsGenericTypeDefinition) d = d.GetGenericTypeDefinition(); + if (cur != null && cur.IsGenericType && !cur.IsGenericTypeDefinition) cur = cur.GetGenericTypeDefinition(); + return d == cur; + } + + string title = FormatClassName(currentType); + + if (currentType == typeof(Integrator)) title = "Global Settings"; + + var group = new ParameterGroup { + Title = title, + Properties = allProps.Where(p => IsCurrentDeclared(p)).ToList(), + Fields = allFields.Where(f => IsCurrentDeclared(f)).ToList() + }; + + if (group.HasParameters) + groups.Add(group); + + currentType = currentType.BaseType; + } + + return groups; + } + + string GetDescription(MemberInfo member) + { + return DocumentationHelper.GetSummary(member) ?? ""; + } + + string FormatClassName(Type t) { + string name = t.Name; + if (name.Contains('`')) name = name.Substring(0, name.IndexOf('`')); + return System.Text.RegularExpressions.Regex.Replace(name, "(\\B[A-Z])", " $1"); + } + + bool IsVisible(MemberInfo m) + { + if (m.Name == "Name" || m.Name == "Disabled") return false; + if (m is PropertyInfo p && (!p.CanRead || !p.CanWrite)) return false; + if (m is FieldInfo f && (f.IsLiteral || f.IsInitOnly)) return false; + + Type t = (m is PropertyInfo pi) ? pi.PropertyType : ((FieldInfo)m).FieldType; + Type underlyingType = Nullable.GetUnderlyingType(t) ?? t; + + return underlyingType == typeof(int) || underlyingType == typeof(long) || + underlyingType == typeof(uint) || underlyingType == typeof(ulong) || + underlyingType == typeof(float) || underlyingType == typeof(double) || + underlyingType == typeof(bool) || underlyingType.IsEnum || IsStruct(underlyingType); + } + + IEnumerable GetFilteredProps(Type type) => + type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); + + IEnumerable GetFilteredFields(Type type) => + type.GetFields(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); + + bool IsStruct(Type t) => + t.IsValueType && !t.IsPrimitive && !t.IsEnum && t != typeof(string) && t != typeof(decimal) && t != typeof(DateTime) && Nullable.GetUnderlyingType(t) == null; + + RenderFragment RenderSetting(MemberInfo member, Func getter, Action setter) => __builder => + { + string name = member.Name; + string desc = GetDescription(member); + + Type type = (member is PropertyInfo p) ? p.PropertyType : ((FieldInfo)member).FieldType; + Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; + + if (underlyingType == typeof(int) || underlyingType == typeof(uint)) + { + int val = Convert.ToInt32(getter() ?? 0); + + + } + else if (underlyingType == typeof(long) || underlyingType == typeof(ulong)) + { + long val = Convert.ToInt64(getter() ?? 0); + + + } + else if (underlyingType == typeof(float) || underlyingType == typeof(double)) + { + float val = Convert.ToSingle(getter() ?? 0.0f); + + + } + else if (underlyingType == typeof(bool)) + { + bool val = Convert.ToBoolean(getter() ?? false); + + + } + }; + + public void TriggerReset() + { + if (CurrentIntegrator != null) + { + var type = CurrentIntegrator.GetType(); + var defaultInstance = Activator.CreateInstance(type); + + if (defaultInstance != null) + { + foreach (var prop in GetFilteredProps(type)) { + prop.SetValue(CurrentIntegrator, prop.GetValue(defaultInstance)); + } + foreach (var field in GetFilteredFields(type)) { + field.SetValue(CurrentIntegrator, field.GetValue(defaultInstance)); + } + StateHasChanged(); + } + } + } + + public async Task TriggerApply() + { + await Task.Yield(); + StateHasChanged(); + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css new file mode 100644 index 00000000..575c6010 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css @@ -0,0 +1,104 @@ +.integrator-selector { + display: flex; + flex-direction: column; + gap: 0.4em; + margin-bottom: 10px; +} + +.integrator-container { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +.integrator-header { + display: flex; + align-items: center; + padding: 4px 0; + cursor: pointer; + user-select: none; + background: transparent; + border: none; +} + +.integrator-content { + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 10px; + margin-top: 4px; +} + +summary { + cursor: pointer; + font-weight: 600; + font-size: 0.85em; + color: #000; + outline: none; + display: flex; + align-items: center; + padding: 4px 0; +} +summary::before { + content: '▶'; + font-size: 0.7em; + margin-right: 6px; + display: inline-block; + transition: transform 0.2s; + color: #000; +} +details[open] > summary::before { transform: rotate(90deg); } +summary::-webkit-details-marker { display: none; } + + +details > div { + margin-left: 6px; + padding-left: 12px; + border-left: 2px solid #e0e0e0; + margin-top: 2px; + margin-bottom: 2px; +} +details:hover > div { + border-left-color: #ccc; +} + + +.struct-details { + width: 100%; + margin-bottom: 2px; +} + +.struct-summary { + cursor: pointer; + list-style: none; + font-size: samller; + font-weight: normal; + color: #000; + background-color: #f5f5f5; + display: flex; + align-items: center; + transition: background-color 0.2s; +} +.struct-summary:hover { + background-color: #e9e9e9; +} +.struct-summary::before { + content: '▶'; + display: inline-block; + font-size: 0.7em; + margin-right: 8px; + color: #000; + transition: transform 0.2s ease; +} +.struct-details[open] > .struct-summary::before { + transform: rotate(90deg); +} + +.struct-content { + margin-left: 10px; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + border-left: 2px solid #e0e0e0; +} + + \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor new file mode 100644 index 00000000..ec2cfcc3 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor @@ -0,0 +1,838 @@ +@page "/ReferenceRendering" +@using SeeSharp.Experiments +@using SeeSharp.Integrators +@using SeeSharp.Blazor +@using System.IO +@using SimpleImageIO +@using System.Reflection +@using System.Threading.Tasks +@using System.Text.Json +@using System.Text.Json.Nodes +@using System.Diagnostics +@using System.Linq +@using System.Text.RegularExpressions; + +
+ +
+ +
+ +
+
Parameters
+
+ +
+ +
+ + +
+
+ +
+
Available references
+
+ @if (referenceFiles.Count == 0) { +
No references found
+ } + @foreach (var file in referenceFiles) { +
+
+ @file.Resolution, max depth: @file.MaxDepth + @if (file.FilePath.EndsWith("-partial.exr")) { + (Partial) + } +
+
+ } +
+ +
+
Render new
+ +
+ +
+ x +
+
+ +
+ + +
+ + + +
+ SPP: + + +
+ +
+ +
Render more
+ +
+ + +
+ + + + +
+
+ +
+
+
Viewer
+
+ @if (flip != null) { + + } + else { + Select a reference or render + } +
+
+ +
+
Info
+
+ @if (selectedFile != null) { +
+ RenderTime: @selectedFile.RenderTimeDisplay + StartTime: @selectedFile.StartTimeDisplay + WriteTime: @selectedFile.WriteTimeDisplay + Iterations: @selectedFile.Spp + Version: @selectedFile.Version +
+ +
+
+ Integrator Settings +
+
+ Integrator: + @selectedFile.IntegratorName +
+ @{ + if (!string.IsNullOrEmpty(selectedFile.RawJsonConfig)) + { + var node = System.Text.Json.Nodes.JsonNode.Parse(selectedFile.RawJsonConfig); + if (node != null) + { + var ignoredKeys = new HashSet { + "RenderTime", "NumIterations", "RenderStartTime", "RenderWriteTime", + "SeeSharpVersion" + }; + foreach (var kvp in node.AsObject()) + { + if (ignoredKeys.Contains(kvp.Key)) continue; + string valStr = kvp.Value?.ToString() ?? "null"; + string typeClass = "val"; + if (kvp.Value is System.Text.Json.Nodes.JsonValue jval) { + if (jval.TryGetValue(out _)) typeClass = "bool"; + else if (jval.TryGetValue(out _)) typeClass = "str"; + } +
+ @kvp.Key: + @valStr +
+ } + } + } + } +
+
+
+ } + else { +
No image selected
+ } +
+
+ +
+
+ +@code { + class ReferenceInfo { + public string FilePath { get; set; } = ""; + public string Resolution { get; set; } = ""; + public int MaxDepth { get; set; } + public string IntegratorName { get; set; } = ""; + + public int Spp { get; set; } + public string RenderTimeDisplay { get; set; } = ""; + public string Version { get; set; } = ""; + public string StartTimeDisplay { get; set; } = ""; + public string WriteTimeDisplay { get; set; } = ""; + public string Timestamp { get; set; } = ""; + + public string RawJsonConfig { get; set; } = ""; + } + + SceneSelector sceneSelector; + IntegratorSelector integratorSelector; + Scene scene; + SceneFromFile currentSceneFile; + string currentSceneDirectory = ""; + FlipBook flip; + + List referenceFiles = new(); + ReferenceInfo selectedFile; + + int renderWidth = 512; + int renderHeight = 512; + int renderMaxDepth = 5; + int quickPreviewSpp = 1; + bool isRendering = false; + int additionalSpp = 32; + + void OnSceneLoaded(SceneFromFile sceneFromFile) + { + currentSceneFile = sceneFromFile; + scene = sceneFromFile.MakeScene(); + + if (!string.IsNullOrEmpty(sceneFromFile.Name)) + { + string sceneName = Path.GetFileNameWithoutExtension(sceneFromFile.Name); + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null && dir.Name != "SeeSharp") + dir = dir.Parent; + if (dir != null) + currentSceneDirectory = Path.Combine(dir.FullName, "Data", "Scenes", sceneName); + + ScanReferences(currentSceneDirectory); + } + } + + void ScanReferences(string sceneDir) + { + referenceFiles.Clear(); + if (string.IsNullOrEmpty(sceneDir)) return; + + string refDir = Path.Combine(sceneDir, "References"); + if (!Directory.Exists(refDir)) return; + + var exrFiles = Directory.GetFiles(refDir, "*.exr") + .OrderByDescending(f => File.GetLastWriteTime(f)); + + foreach (var f in exrFiles) { + var info = new ReferenceInfo { + FilePath = f, + Resolution = GetResolution(f), + Timestamp = File.GetLastWriteTime(f).ToString("yyyy-MM-dd HH:mm:ss") + }; + + ReadMetadataFromJson(info, f); + + referenceFiles.Add(info); + } + } + + void ReadMetadataFromJson(ReferenceInfo info, string exrPath) + { + string folder = Path.GetDirectoryName(exrPath); + string fileNameNoExt = Path.GetFileNameWithoutExtension(exrPath); + string jsonPath = Path.Combine(folder, $"{fileNameNoExt}.json"); + + if (!File.Exists(jsonPath)) return; + + string jsonContent = File.ReadAllText(jsonPath); + var root = JsonNode.Parse(jsonContent); + if (root == null) return; + + info.Version = root["SeeSharpVersion"]?.ToString() ?? ""; + info.StartTimeDisplay = root["RenderStartTime"]?.ToString() ?? ""; + info.WriteTimeDisplay = root["RenderWriteTime"]?.ToString() ?? ""; + + if (root["RenderTime"] != null) { + double ms = root["RenderTime"].GetValue(); + TimeSpan t = TimeSpan.FromMilliseconds(ms); + info.RenderTimeDisplay = t.TotalMinutes >= 1 ? $"{t.Minutes:D2}m {t.Seconds:D2}s" : $"{ms:F0}ms"; + } + + if (root["NumIterations"] != null) info.Spp = root["NumIterations"].GetValue(); + + var settingsNode = root["Settings"]; + if (settingsNode != null) { + var options = new JsonSerializerOptions { WriteIndented = true }; + info.RawJsonConfig = settingsNode.ToJsonString(options); + info.MaxDepth = settingsNode["MaxDepth"]?.GetValue() ?? 0; + } + + string integratorName = root["Name"]?.GetValue(); + info.IntegratorName = integratorName.Split('.').Last(); + } + + string GetResolution(string filePath) + { + string filename = Path.GetFileNameWithoutExtension(filePath); + var match = Regex.Match(filename, @"Width(\d+)-Height(\d+)", RegexOptions.IgnoreCase); + if (match.Success) return $"{match.Groups[1].Value}x{match.Groups[2].Value}"; + + return "Unknown"; + } + + void ResetConfig() + { + integratorSelector?.TriggerReset(); + } + + async Task ApplyConfig() + { + if (integratorSelector == null) return; + + string folder = Path.GetDirectoryName(selectedFile?.FilePath) ?? currentSceneDirectory ?? ""; + if (!folder.EndsWith("References")) { + string refDir = Path.Combine(folder, "References"); + if (Directory.Exists(refDir)) + folder = refDir; + } + string configPath = Path.Combine(folder, "Config.json"); + if (File.Exists(configPath)) { + if (LoadIntegratorFromJson(configPath)) + return; + } + } + + bool LoadIntegratorFromJson(string path) { + if (!File.Exists(path)) return false; + + string json = File.ReadAllText(path); + var root = JsonNode.Parse(json); + + JsonNode settingsNode = root["Settings"]; + + if (settingsNode == null) return false; + + var currentIntegrator = integratorSelector.addedIntegrators.FirstOrDefault(); + if (currentIntegrator == null) return false; + + var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; + var loaded = JsonSerializer.Deserialize(settingsNode, currentIntegrator.GetType(), options); + + CopyProperties(currentIntegrator, loaded); + StateHasChanged(); + return true; + } + + void CopyProperties(object target, object source) { + if (target == null || source == null || target.GetType() != source.GetType()) return; + var type = target.GetType(); + + bool IsConfigParam(Type t) { + return t == typeof(string) || t.IsValueType; + } + + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite)) { + if (IsConfigParam(prop.PropertyType)) + prop.SetValue(target, prop.GetValue(source)); + } + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { + if (IsConfigParam(field.FieldType)) + field.SetValue(target, field.GetValue(source)); + } + } + + void SelectReference(ReferenceInfo file) + { + selectedFile = file; + + string folder = Path.GetDirectoryName(file.FilePath); + string configPath = Path.Combine(folder, "Config.json"); + int configTargetSpp = 0; + + if (File.Exists(configPath)) { + var root = JsonNode.Parse(File.ReadAllText(configPath)); + var settings = root["Settings"]; + configTargetSpp = settings?["TotalSpp"]?.GetValue() + ?? settings?["NumIterations"]?.GetValue() + ?? 0; + } + + bool isPartial = file.FilePath.EndsWith("-partial.exr"); + if (isPartial && configTargetSpp > file.Spp) { + additionalSpp = configTargetSpp - file.Spp; + } + + if (File.Exists(file.FilePath)) { + UpdateViewerFromFile(file.FilePath); + } + } + + void UpdateViewerFromFile(string path) + { + flip = null; + StateHasChanged(); + + if (!File.Exists(path)) return; + var layers = SimpleImageIO.Layers.LoadFromFile(path); + var img = layers.Values.OfType().FirstOrDefault(); + + if (img != null) + UpdatePreviewFromMemory(img); + } + + async void UpdatePreviewFromMemory(RgbImage image) { + var imgClone = new RgbImage(image.Width, image.Height); + CopyImage(imgClone, image); + flip = null; + StateHasChanged(); + await Task.Delay(1); + var newFlip = new FlipBook(660, 580); + newFlip.Add("", imgClone); + flip = newFlip; + StateHasChanged(); + } + + void CopyImage(RgbImage target, RgbImage source) { + Parallel.For(0, target.Height, y => { + for (int x = 0; x < target.Width; ++x) + target.SetPixel(x, y, source.GetPixel(x, y)); + }); + } + + async Task RenderReference() + { + if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) return; + + isRendering = true; + flip = null; + await InvokeAsync(StateHasChanged); + + var curIntegrator = integratorSelector.addedIntegrators.First(); + + await Task.Run(async () => { + var startTime = DateTime.Now; + var stopwatch = Stopwatch.StartNew(); + + var renderScene = scene; + var renderIntegrator = CloneIntegrator(curIntegrator); + + SetMaxDepth(renderIntegrator, renderMaxDepth); + int targetSpp = GetTargetSpp(renderIntegrator); + + string referencesRoot = Path.Combine(currentSceneDirectory, "References"); + Directory.CreateDirectory(referencesRoot); + + SaveConfig(referencesRoot, renderIntegrator); + + string baseName = $"MaxDepth{renderMaxDepth}-Width{renderWidth}-Height{renderHeight}"; + string finalPath = Path.Combine(referencesRoot, baseName + ".exr"); + string partialPath = Path.Combine(referencesRoot, baseName + "-partial.exr"); + + var accumulator = new RgbImage(renderWidth, renderHeight); + renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); + renderScene.Prepare(); + + int currentSpp = 0; + int nextTargetSpp = 1; + + while (currentSpp < targetSpp) + { + int delta = nextTargetSpp - currentSpp; + if (currentSpp + delta > targetSpp) delta = targetSpp - currentSpp; + if (delta <= 0) break; + + SetBatchSpp(renderIntegrator, delta); + + renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); + renderIntegrator.Render(renderScene); + + var batchImg = new RgbImage(renderWidth, renderHeight); + CopyImage(batchImg, renderScene.FrameBuffer.Image); + batchImg.Scale((float)delta); + AddImage(accumulator, batchImg); + + currentSpp += delta; + + if (currentSpp == nextTargetSpp || currentSpp == targetSpp) { + SetBatchSpp(renderIntegrator, currentSpp); + WriteImageWithJson(accumulator, partialPath, renderIntegrator, currentSpp, renderMaxDepth, stopwatch.ElapsedMilliseconds, startTime); + + if (currentSpp < targetSpp) nextTargetSpp *= 2; + if (nextTargetSpp > targetSpp) nextTargetSpp = targetSpp; + } + } + stopwatch.Stop(); + + SetBatchSpp(renderIntegrator, targetSpp); + WriteImageWithJson(accumulator, finalPath, renderIntegrator, targetSpp, renderMaxDepth, stopwatch.ElapsedMilliseconds, startTime); + + if (File.Exists(partialPath)) File.Delete(partialPath); + string partialJson = Path.ChangeExtension(partialPath, ".json"); + if (File.Exists(partialJson)) File.Delete(partialJson); + + var newRef = new ReferenceInfo { + FilePath = finalPath, + Resolution = $"{renderWidth}x{renderHeight}", + MaxDepth = renderMaxDepth, + Spp = targetSpp, + Version = typeof(SeeSharp.Scene).Assembly.GetName().Version?.ToString() ?? "Unknown", + StartTimeDisplay = startTime.ToString("yyyy-MM-dd HH:mm:ss"), + WriteTimeDisplay = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), + Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), + IntegratorName = renderIntegrator.GetType().Name + }; + + double ms = stopwatch.ElapsedMilliseconds; + TimeSpan t = TimeSpan.FromMilliseconds(ms); + newRef.RenderTimeDisplay = t.TotalMinutes >= 1 ? $"{t.Minutes:D2}m {t.Seconds:D2}s" : $"{ms:F0}ms"; + + newRef.RawJsonConfig = JsonSerializer.Serialize(renderIntegrator, renderIntegrator.GetType(), new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); + + await InvokeAsync(() => { + var existing = referenceFiles.FirstOrDefault(r => r.FilePath == finalPath); + if (existing != null) referenceFiles.Remove(existing); + + referenceFiles.Insert(0, newRef); + selectedFile = newRef; + UpdateViewerFromFile(finalPath); + StateHasChanged(); + }); + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + async Task RenderMoreSamples() + { + if (currentSceneFile == null || selectedFile == null || !File.Exists(selectedFile.FilePath)) return; + if (integratorSelector.addedIntegrators.Count == 0) return; + + isRendering = true; + await InvokeAsync(StateHasChanged); + var curIntegrator = integratorSelector.addedIntegrators.First(); + + await Task.Run(async () => { + long previousRenderTimeMs = 0; + DateTime originalStartTime = DateTime.Now; + uint originalBaseSeed = 0; + Integrator renderIntegrator = null; + + string folder = Path.GetDirectoryName(selectedFile.FilePath); + string fileNameNoExt = Path.GetFileNameWithoutExtension(selectedFile.FilePath); + string jsonPath = Path.Combine(folder, $"{fileNameNoExt}.json"); + + if (File.Exists(jsonPath)) { + try { + var root = JsonNode.Parse(File.ReadAllText(jsonPath)); + if (root != null) { + previousRenderTimeMs = (long)(root["RenderTime"]?.GetValue() ?? 0); + string startStr = root["RenderStartTime"]?.ToString(); + if (!DateTime.TryParse(startStr, out originalStartTime)) + DateTime.TryParseExact(startStr, "dd/M/yyyy HH:mm:ss", null, System.Globalization.DateTimeStyles.None, out originalStartTime); + + var settingsNode = root["Settings"]; + var uiType = integratorSelector.addedIntegrators.FirstOrDefault()?.GetType(); + if (settingsNode != null && uiType != null) { + var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; + renderIntegrator = JsonSerializer.Deserialize(settingsNode, uiType, options) as Integrator; + } + } + } catch {} + } + + if (renderIntegrator == null) { + string configPath = Path.Combine(folder, "Config.json"); + if (File.Exists(configPath)) { + var root = JsonNode.Parse(File.ReadAllText(configPath)); + var settingsNode = root["Settings"]; + var uiType = integratorSelector.addedIntegrators.FirstOrDefault()?.GetType(); + if (settingsNode != null && uiType != null) { + var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; + renderIntegrator = JsonSerializer.Deserialize(settingsNode, uiType, options) as Integrator; + } + } + } + + var stopwatch = Stopwatch.StartNew(); + + var oldImg = new RgbImage(selectedFile.FilePath); + int oldSpp = selectedFile.Spp; + int targetTotalSpp = oldSpp + additionalSpp; + + var accumulator = new RgbImage(oldImg.Width, oldImg.Height); + CopyImage(accumulator, oldImg); + accumulator.Scale((float)oldSpp); + + var renderScene = scene; + + originalBaseSeed = GetBaseSeed(renderIntegrator); + SetBaseSeed(renderIntegrator, originalBaseSeed + (uint)oldSpp); + + SetMaxDepth(renderIntegrator, selectedFile.MaxDepth); + + int currentSpp = oldSpp; + int nextTargetSpp = currentSpp + 1; + + renderScene.FrameBuffer = new FrameBuffer(oldImg.Width, oldImg.Height, ""); + renderScene.Prepare(); + + string currentPath = selectedFile.FilePath; + string finalPath, partialPath; + + if (currentPath.EndsWith("-partial.exr")) { + partialPath = currentPath; + finalPath = currentPath.Replace("-partial.exr", ".exr"); + } else { + finalPath = currentPath; + partialPath = Path.ChangeExtension(currentPath, "-partial.exr"); + } + + while (currentSpp < targetTotalSpp) + { + int delta = nextTargetSpp - currentSpp; + if (currentSpp + delta > targetTotalSpp) delta = targetTotalSpp - currentSpp; + if (delta <= 0) break; + + SetBatchSpp(renderIntegrator, delta); + renderScene.FrameBuffer = new FrameBuffer(oldImg.Width, oldImg.Height, ""); + renderIntegrator.Render(renderScene); + + var batchImg = new RgbImage(oldImg.Width, oldImg.Height); + CopyImage(batchImg, renderScene.FrameBuffer.Image); + batchImg.Scale((float)delta); + AddImage(accumulator, batchImg); + + currentSpp += delta; + long totalTimeNow = previousRenderTimeMs + stopwatch.ElapsedMilliseconds; + + SetBaseSeed(renderIntegrator, originalBaseSeed); + SetBatchSpp(renderIntegrator, currentSpp); + WriteImageWithJson(accumulator, partialPath, renderIntegrator, currentSpp, renderMaxDepth, totalTimeNow, originalStartTime); + + SetBaseSeed(renderIntegrator, originalBaseSeed + (uint)currentSpp); + + if (currentSpp < targetTotalSpp) nextTargetSpp += Math.Max(1, (nextTargetSpp - oldSpp) * 2); + if (nextTargetSpp > targetTotalSpp) nextTargetSpp = targetTotalSpp; + } + stopwatch.Stop(); + + long finalTotalTime = previousRenderTimeMs + stopwatch.ElapsedMilliseconds; + + SetBaseSeed(renderIntegrator, originalBaseSeed); + SetBatchSpp(renderIntegrator, targetTotalSpp); + WriteImageWithJson(accumulator, finalPath, renderIntegrator, targetTotalSpp, renderMaxDepth, finalTotalTime, originalStartTime); + + if (finalPath != partialPath) { + if (File.Exists(partialPath)) File.Delete(partialPath); + string pJson = Path.ChangeExtension(partialPath, ".json"); + if (File.Exists(pJson)) File.Delete(pJson); + } + + var newRef = new ReferenceInfo { FilePath = finalPath }; + ReadMetadataFromJson(newRef, finalPath); + + newRef.Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + newRef.StartTimeDisplay = originalStartTime.ToString("yyyy-MM-dd HH:mm:ss"); + newRef.WriteTimeDisplay = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + newRef.Spp = targetTotalSpp; + newRef.Resolution = $"{oldImg.Width}x{oldImg.Height}"; + + TimeSpan t = TimeSpan.FromMilliseconds(finalTotalTime); + newRef.RenderTimeDisplay = t.TotalMinutes >= 1 ? $"{t.Minutes:D2}m {t.Seconds:D2}s" : $"{finalTotalTime}ms"; + newRef.RawJsonConfig = JsonSerializer.Serialize(renderIntegrator, renderIntegrator.GetType(), new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); + + await InvokeAsync(() => { + var existing = referenceFiles.FirstOrDefault(r => r.FilePath == currentPath); + if (existing != null) referenceFiles.Remove(existing); + + var existingFinal = referenceFiles.FirstOrDefault(r => r.FilePath == finalPath); + if (existingFinal != null) referenceFiles.Remove(existingFinal); + + referenceFiles.Insert(0, newRef); + selectedFile = newRef; + + UpdateViewerFromFile(finalPath); + StateHasChanged(); + }); + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + async Task RenderQuickPreview() + { + if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) return; + + isRendering = true; + selectedFile = null; + flip = null; + + await InvokeAsync(StateHasChanged); + + var curIntegrator = integratorSelector.addedIntegrators.First(); + + await Task.Run(async () => { + var stopwatch = Stopwatch.StartNew(); + var renderScene = currentSceneFile.MakeScene(); + var renderIntegrator = CloneIntegrator(curIntegrator); + + int targetSpp = quickPreviewSpp < 1 ? 1 : quickPreviewSpp; + SetBatchSpp(renderIntegrator, targetSpp); + SetMaxDepth(renderIntegrator, renderMaxDepth); + + renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); + renderScene.Prepare(); + renderIntegrator.Render(renderScene); + + stopwatch.Stop(); + + var resultImg = new RgbImage(renderWidth, renderHeight); + CopyImage(resultImg, renderScene.FrameBuffer.Image); + + await InvokeAsync(() => { + UpdatePreviewFromMemory(resultImg); + }); + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + Integrator CloneIntegrator(Integrator source) { + var type = source.GetType(); + var clone = (Integrator)Activator.CreateInstance(type); + bool IsConfigParam(Type t) { + return t == typeof(string) || t.IsValueType; + } + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite)) + if (IsConfigParam(prop.PropertyType)) + prop.SetValue(clone, prop.GetValue(source)); + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + if (IsConfigParam(field.FieldType)) + field.SetValue(clone, field.GetValue(source)); + return clone; + } + + void SetMaxDepth(Integrator integrator, int depth) { + var type = integrator.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var prop = type.GetProperty("MaxDepth", flags); + if (prop != null && prop.CanWrite) { + prop.SetValue(integrator, depth); + return; + } + var field = type.GetField("MaxDepth", flags); + if (field != null) { + field.SetValue(integrator, depth); + } + } + + int GetTargetSpp(Integrator integrator) { + var type = integrator.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var propSpp = type.GetProperty("TotalSpp", flags); + if (propSpp != null) + return (int)propSpp.GetValue(integrator); + + var fieldSpp = type.GetField("TotalSpp", flags); + if (fieldSpp != null) + return (int)fieldSpp.GetValue(integrator); + + var propIter = type.GetProperty("NumIterations", flags); + if (propIter != null) + return (int)propIter.GetValue(integrator); + + var fieldIter = type.GetField("NumIterations", flags); + if (fieldIter != null) + return (int)fieldIter.GetValue(integrator); + return 16; + } + + void SaveConfig(string folder, Integrator integrator) { + var options = new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }; + var rootNode = new JsonObject(); + rootNode.Add("Name", integrator.GetType().Name); + var settingsNode = JsonSerializer.SerializeToNode(integrator, integrator.GetType(), options); + rootNode.Add("Settings", settingsNode); + File.WriteAllText(Path.Combine(folder, "Config.json"), rootNode.ToJsonString(options)); + } + + void SetBatchSpp(Integrator integrator, int batchCount) { + var type = integrator.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var targetNames = new[] { "TotalSpp", "NumIterations"}; + foreach (var name in targetNames) { + var prop = type.GetProperty(name, flags); + if (prop != null && prop.CanWrite) { + prop.SetValue(integrator, batchCount); + return; + } + var field = type.GetField(name, flags); + if (field != null) { + field.SetValue(integrator, batchCount); + return; + } + } + } + + void AddImage(RgbImage target, RgbImage source) { + Parallel.For(0, target.Height, y => { + for (int x = 0; x < target.Width; ++x) + target.SetPixel(x, y, target.GetPixel(x, y) + source.GetPixel(x, y)); + }); + } + + void WriteImageWithJson(RgbImage image, string filename, Integrator integrator, int spp, int depth, long timeMs, DateTime startTime) + { + var output = new RgbImage(image.Width, image.Height); + CopyImage(output, image); + if (spp > 0) output.Scale(1.0f / spp); + output.WriteToFile(filename); + + var rootNode = new JsonObject(); + rootNode.Add("RenderTime", timeMs); + rootNode.Add("NumIterations", spp); + rootNode.Add("RenderStartTime", startTime.ToString("yyyy-MM-dd HH:mm:ss")); + rootNode.Add("RenderWriteTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + rootNode.Add("SeeSharpVersion", typeof(Scene).Assembly.GetName().Version?.ToString() ?? "Unknown"); + rootNode.Add("Name", integrator.GetType().Name); + + var options = new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }; + var settingsNode = JsonSerializer.SerializeToNode(integrator, integrator.GetType(), options); + rootNode.Add("Settings", settingsNode); + + string jsonPath = Path.ChangeExtension(filename, ".json"); + File.WriteAllText(jsonPath, rootNode.ToJsonString(options)); + } + + uint GetBaseSeed(Integrator integrator) { + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var seedProp = integrator.GetType().GetProperty("BaseSeed", flags); + var seedField = integrator.GetType().GetField("BaseSeed", flags); + if (seedProp != null) return (uint)(seedProp.GetValue(integrator) ?? 0u); + if (seedField != null) return (uint)(seedField.GetValue(integrator) ?? 0u); + return 0; + } + + void SetBaseSeed(Integrator integrator, uint seed) { + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var seedProp = integrator.GetType().GetProperty("BaseSeed", flags); + var seedField = integrator.GetType().GetField("BaseSeed", flags); + if (seedProp != null) seedProp.SetValue(integrator, seed); + else if (seedField != null) seedField.SetValue(integrator, seed); + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css new file mode 100644 index 00000000..3585df77 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css @@ -0,0 +1,159 @@ +.experiment-layout { + display: grid; + grid-template-columns: 350px 350px 1fr; + grid-template-rows: auto 1fr; + gap: 15px; + height: calc(100vh - 80px); + padding: 10px; + box-sizing: border-box; +} + +.layout-header { + grid-column: 1 / -1; + background: transparent; +} + +.col-box { + display: flex; + flex-direction: column; + border: 1px solid #ccc; + background: white; + overflow: hidden; + height: 600px; +} + +.col-title { + padding: 8px 8px; + background: #eee; + border-bottom: 1px solid #ddd; + font-weight: bold; + font-size: 1em; +} + +.col-content { + flex-grow: 1; + overflow-y: auto; + padding: 10px; +} + +.btn-success-anim { + background-color: #28a745 !important; + border-color: #28a745 !important; + color: white !important; + transition: all 0.1s ease; +} + +.ref-item { + padding: 1px; + border: 1px solid #eee; + margin-bottom: 2px; + cursor: pointer; + transition: background 0.2s; +} +.ref-item:hover { background-color: #f5f5f5; } +.ref-item.active { background-color: #e3f2fd; border-color: #2196F3; } + +.render-form { + border-top: 1px solid #ddd; + padding: 8px; +} + +.form-group { + margin-bottom: 5px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: smaller; +} +.form-group input { width: 70px; padding: 2px; } + +.right-col-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + height: auto; + overflow: visible; +} + +.viewer-box { + height: 600px; + display: flex; + flex-direction: column; + border: 1px solid #ccc; + background: white; + overflow: hidden; +} + +.viewer-area { + flex: 1; + width: 100%; + background-color: #2e2e2e; + position: relative; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.slider-area { + flex: 0 0 auto; + padding: 5px 10px; + background: #f1f1f1; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + z-index: 5; +} + +.meta-box { + height: auto; + min-height: 100px; + border: 1px solid #ccc; + background: white; + display: flex; + flex-direction: column; + margin-bottom: 10px; +} + +.meta-content { + padding: 5px 5px; + overflow-y: auto; + flex: 1; +} + +.meta-grid { + display: grid; + grid-template-columns: auto 1fr; + column-gap: 10px; + row-gap: 5px; + font-size: smaller; + align-items: center; +} +.meta-grid .label { + font-weight: bold; + text-align: right; +} + +.empty-tip { + color: #ccc; + text-align: center; + margin-top: 40px; + font-style: italic; +} + +.config-tree { + margin-left: 4px; + padding-left: 10px; + border-left: 2px solid #e0e0e0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.config-item { + font-size: smaller; + display: flex; +} + +.config-key { + margin-right: 6px; +} diff --git a/SeeSharp.ReferenceManager/Pages/_Host.cshtml b/SeeSharp.ReferenceManager/Pages/_Host.cshtml new file mode 100644 index 00000000..356b5c71 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/_Host.cshtml @@ -0,0 +1,34 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@namespace SeeSharp.ReferenceManager.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + @Html.Raw(SeeSharp.Blazor.Scripts.AllScripts) + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/SeeSharp.ReferenceManager/Program.cs b/SeeSharp.ReferenceManager/Program.cs new file mode 100644 index 00000000..ffae204c --- /dev/null +++ b/SeeSharp.ReferenceManager/Program.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +SceneRegistry.AddSourceRelativeToScript("../Data/Scenes"); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +app.Run(); diff --git a/SeeSharp.ReferenceManager/SeeSharp.ReferenceManager.csproj b/SeeSharp.ReferenceManager/SeeSharp.ReferenceManager.csproj new file mode 100644 index 00000000..ce65c30d --- /dev/null +++ b/SeeSharp.ReferenceManager/SeeSharp.ReferenceManager.csproj @@ -0,0 +1,17 @@ + + + + Exe + net9.0 + enable + + + + + + + + + + + diff --git a/SeeSharp.ReferenceManager/_Imports.razor b/SeeSharp.ReferenceManager/_Imports.razor new file mode 100644 index 00000000..178485ed --- /dev/null +++ b/SeeSharp.ReferenceManager/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using SeeSharp.ReferenceManager \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/appsettings.Development.json b/SeeSharp.ReferenceManager/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/SeeSharp.ReferenceManager/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/SeeSharp.ReferenceManager/appsettings.json b/SeeSharp.ReferenceManager/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/SeeSharp.ReferenceManager/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/SeeSharp.ReferenceManager/wwwroot/css/site.css b/SeeSharp.ReferenceManager/wwwroot/css/site.css new file mode 100644 index 00000000..ddc98cca --- /dev/null +++ b/SeeSharp.ReferenceManager/wwwroot/css/site.css @@ -0,0 +1,86 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 3.5rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +html { + font-family: system-ui; +} + +button { + background-color: #a4e1f2; + border-style: none; + /* border-width: 2px; + border-color: #245e6f; */ + color: black; + font-size: medium; + padding-left: 8px; + padding-right: 8px; + padding-bottom: 4px; + padding-top: 4px; +} + button:hover { + background-color: #c9eff4; + cursor: pointer; + } + button:disabled { + background-color: #e5f1f5; + color: #96b4bd; + border-color: #96b4bd; + } + +.experiment-settings { + display: flex; + gap: 0.25em; + flex-direction: column; + float: left; + margin-right: 1em; +} + +.experiment-results { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: flex-start; +} + +table { + border-collapse: collapse; +} +td, th { + border: none; + padding: 4px; +} +tr:hover { background-color: #e7f2f1; } +th { + padding-top: 6px; + padding-bottom: 6px; + text-align: left; + background-color: #4a96af; + color: white; + font-size: smaller; +} \ No newline at end of file From 4bad8cb63216dd2968f4c26e4d60356f003a2d6c Mon Sep 17 00:00:00 2001 From: Pascal Grittmann Date: Thu, 15 Jan 2026 14:51:56 +0100 Subject: [PATCH 2/3] add project to sln --- SeeSharp.sln | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/SeeSharp.sln b/SeeSharp.sln index c0161e52..aa5cbbc6 100644 --- a/SeeSharp.sln +++ b/SeeSharp.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Blazor", "SeeSharp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialTest", "MaterialTest\MaterialTest.csproj", "{5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.ReferenceManager", "SeeSharp.ReferenceManager\SeeSharp.ReferenceManager.csproj", "{EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,9 +36,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -170,5 +169,20 @@ Global {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x64.Build.0 = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.ActiveCfg = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.Build.0 = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x64.Build.0 = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Debug|x86.Build.0 = Debug|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|Any CPU.Build.0 = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x64.ActiveCfg = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x64.Build.0 = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x86.ActiveCfg = Release|Any CPU + {EBF9A5F3-AE1D-4795-98CA-6D8B6CDF08A4}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal From 96c891cca1cdc31a9fc1a7e9ed0ad81e113c254d Mon Sep 17 00:00:00 2001 From: Mengzhu Wang Date: Wed, 21 Jan 2026 15:13:40 +0100 Subject: [PATCH 3/3] separate logic from UI --- .../Pages/IntegratorSelector.razor | 213 +---- .../Pages/IntegratorSelector.razor.cs | 88 ++ .../Pages/IntegratorSelector.razor.css | 58 -- .../Pages/IntegratorUtils.cs | 80 ++ .../Pages/ReferenceRendering.razor | 766 ++---------------- .../Pages/ReferenceRendering.razor.cs | 406 ++++++++++ .../Pages/ReferenceRendering.razor.css | 16 - .../Pages/ReferenceUtils.cs | 249 ++++++ .../Pages/RenderSetting.razor | 41 + .../Common/DocumentationReader.cs | 17 +- 10 files changed, 971 insertions(+), 963 deletions(-) create mode 100644 SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.cs create mode 100644 SeeSharp.ReferenceManager/Pages/IntegratorUtils.cs create mode 100644 SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.cs create mode 100644 SeeSharp.ReferenceManager/Pages/ReferenceUtils.cs create mode 100644 SeeSharp.ReferenceManager/Pages/RenderSetting.razor rename SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs => SeeSharp/Common/DocumentationReader.cs (78%) diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor index 52b77f29..de298dcd 100644 --- a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor @@ -9,10 +9,10 @@
- @foreach (var type in integratorTypes) { - + }
@@ -33,211 +33,20 @@
@foreach (var prop in group.Properties) { - @RenderSetting(prop, () => prop.GetValue(CurrentIntegrator), v => prop.SetValue(CurrentIntegrator, v)) + } @foreach (var field in group.Fields) { - @RenderSetting(field, () => field.GetValue(CurrentIntegrator), v => field.SetValue(CurrentIntegrator, v)) + }
} } -
- -@code { - [Parameter] public Scene scene { get; set; } = default!; - - public List addedIntegrators { get; private set; } = new(); - - public Integrator? CurrentIntegrator => addedIntegrators.FirstOrDefault(); - - Type[] integratorTypes = Array.Empty(); - string? selectedIntegrator; - - class ParameterGroup - { - public string Title { get; set; } = ""; - public List Properties { get; set; } = new(); - public List Fields { get; set; } = new(); - public bool HasParameters => Properties.Any() || Fields.Any(); - } - - protected override void OnInitialized() - { - integratorTypes = new Type[] {typeof(PathTracer), typeof(VertexConnectionAndMerging)}; - if (integratorTypes.Length > 0) { - selectedIntegrator = integratorTypes[0].FullName; - ReplaceIntegrator(); - } - DocumentationHelper.LoadXmlDocumentation(typeof(Integrator).Assembly); - } - - void OnSelectionChanged(ChangeEventArgs e) - { - selectedIntegrator = e.Value?.ToString(); - if (!string.IsNullOrEmpty(selectedIntegrator)) - { - ReplaceIntegrator(); - } - } - - void ReplaceIntegrator() - { - if (string.IsNullOrEmpty(selectedIntegrator)) return; - var type = integratorTypes.FirstOrDefault(t => t.FullName == selectedIntegrator); - if (type == null) return; - - addedIntegrators.Clear(); - - var integrator = (Integrator)Activator.CreateInstance(type)!; - addedIntegrators.Add(integrator); - - StateHasChanged(); - } - - List GetParameterGroups(Integrator integrator) - { - var groups = new List(); - var currentType = integrator.GetType(); - - var allProps = GetFilteredProps(currentType); - var allFields = GetFilteredFields(currentType); - - while (currentType != null && currentType != typeof(object)) { - bool IsCurrentDeclared(MemberInfo m) - { - var d = m.DeclaringType; - var cur = currentType; - if (d != null && d.IsGenericType && !d.IsGenericTypeDefinition) d = d.GetGenericTypeDefinition(); - if (cur != null && cur.IsGenericType && !cur.IsGenericTypeDefinition) cur = cur.GetGenericTypeDefinition(); - return d == cur; - } - - string title = FormatClassName(currentType); - - if (currentType == typeof(Integrator)) title = "Global Settings"; - - var group = new ParameterGroup { - Title = title, - Properties = allProps.Where(p => IsCurrentDeclared(p)).ToList(), - Fields = allFields.Where(f => IsCurrentDeclared(f)).ToList() - }; - - if (group.HasParameters) - groups.Add(group); - - currentType = currentType.BaseType; - } - - return groups; - } - - string GetDescription(MemberInfo member) - { - return DocumentationHelper.GetSummary(member) ?? ""; - } - - string FormatClassName(Type t) { - string name = t.Name; - if (name.Contains('`')) name = name.Substring(0, name.IndexOf('`')); - return System.Text.RegularExpressions.Regex.Replace(name, "(\\B[A-Z])", " $1"); - } - - bool IsVisible(MemberInfo m) - { - if (m.Name == "Name" || m.Name == "Disabled") return false; - if (m is PropertyInfo p && (!p.CanRead || !p.CanWrite)) return false; - if (m is FieldInfo f && (f.IsLiteral || f.IsInitOnly)) return false; - - Type t = (m is PropertyInfo pi) ? pi.PropertyType : ((FieldInfo)m).FieldType; - Type underlyingType = Nullable.GetUnderlyingType(t) ?? t; - - return underlyingType == typeof(int) || underlyingType == typeof(long) || - underlyingType == typeof(uint) || underlyingType == typeof(ulong) || - underlyingType == typeof(float) || underlyingType == typeof(double) || - underlyingType == typeof(bool) || underlyingType.IsEnum || IsStruct(underlyingType); - } - - IEnumerable GetFilteredProps(Type type) => - type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); - - IEnumerable GetFilteredFields(Type type) => - type.GetFields(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); - - bool IsStruct(Type t) => - t.IsValueType && !t.IsPrimitive && !t.IsEnum && t != typeof(string) && t != typeof(decimal) && t != typeof(DateTime) && Nullable.GetUnderlyingType(t) == null; - - RenderFragment RenderSetting(MemberInfo member, Func getter, Action setter) => __builder => - { - string name = member.Name; - string desc = GetDescription(member); - - Type type = (member is PropertyInfo p) ? p.PropertyType : ((FieldInfo)member).FieldType; - Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; - - if (underlyingType == typeof(int) || underlyingType == typeof(uint)) - { - int val = Convert.ToInt32(getter() ?? 0); - - - } - else if (underlyingType == typeof(long) || underlyingType == typeof(ulong)) - { - long val = Convert.ToInt64(getter() ?? 0); - - - } - else if (underlyingType == typeof(float) || underlyingType == typeof(double)) - { - float val = Convert.ToSingle(getter() ?? 0.0f); - - - } - else if (underlyingType == typeof(bool)) - { - bool val = Convert.ToBoolean(getter() ?? false); - - - } - }; - - public void TriggerReset() - { - if (CurrentIntegrator != null) - { - var type = CurrentIntegrator.GetType(); - var defaultInstance = Activator.CreateInstance(type); - - if (defaultInstance != null) - { - foreach (var prop in GetFilteredProps(type)) { - prop.SetValue(CurrentIntegrator, prop.GetValue(defaultInstance)); - } - foreach (var field in GetFilteredFields(type)) { - field.SetValue(CurrentIntegrator, field.GetValue(defaultInstance)); - } - StateHasChanged(); - } - } - } - - public async Task TriggerApply() - { - await Task.Yield(); - StateHasChanged(); - } -} \ No newline at end of file + \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.cs b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.cs new file mode 100644 index 00000000..a44bbae1 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Components; + +namespace SeeSharp.ReferenceManager.Pages; + +public partial class IntegratorSelector : ComponentBase +{ + [Parameter] public Scene scene { get; set; } = default!; + + public List addedIntegrators { get; private set; } = new(); + + public Integrator? CurrentIntegrator => addedIntegrators.FirstOrDefault(); + + Type[] integratorTypes = Array.Empty(); + string? selectedIntegrator; + private string lastIntegrator; + + protected override void OnInitialized() + { + var types = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && !type.IsAbstract && typeof(Integrator).IsAssignableFrom(type) && + !type.ContainsGenericParameters && !typeof(DebugVisualizer).IsAssignableFrom(type)); + integratorTypes = types.Where(t => !types.Any(other => other.IsSubclassOf(t))).ToArray(); + + if (integratorTypes.Length > 0) { + selectedIntegrator = integratorTypes[0].FullName; + lastIntegrator = selectedIntegrator; + ReplaceIntegrator(); + } + DocumentationReader.LoadXmlDocumentation(typeof(Integrator).Assembly); + } + + void OnSelectionChanged(ChangeEventArgs e) + { + selectedIntegrator = e.Value?.ToString(); + if (!string.IsNullOrEmpty(selectedIntegrator)) + { + lastIntegrator = selectedIntegrator; + ReplaceIntegrator(); + } + } + + void ReplaceIntegrator() + { + if (string.IsNullOrEmpty(selectedIntegrator)) return; + var type = integratorTypes.FirstOrDefault(t => t.FullName == selectedIntegrator); + if (type == null) return; + + addedIntegrators.Clear(); + + var integrator = (Integrator)Activator.CreateInstance(type)!; + addedIntegrators.Add(integrator); + + StateHasChanged(); + } + + protected List GetParameterGroups(Integrator integrator) + => IntegratorUtils.GetParameterGroups(integrator); + + protected string FormatClassName(Type t) => IntegratorUtils.FormatClassName(t); + + public void TriggerReset() + { + selectedIntegrator = lastIntegrator; + ReplaceIntegrator(); + } + + public bool TrySelectIntegrator(string simpleName) + { + var targetType = integratorTypes.FirstOrDefault(t => t.Name == simpleName || t.Name == simpleName + "`1"); + + if (targetType != null && targetType.FullName != selectedIntegrator) + { + selectedIntegrator = targetType.FullName; + ReplaceIntegrator(); + return true; + } + return targetType != null; + } + + public Type? GetIntegratorType(string simpleName) + { + if (string.IsNullOrEmpty(simpleName)) return null; + return integratorTypes.FirstOrDefault(t => + t.Name.Equals(simpleName, StringComparison.OrdinalIgnoreCase) || + t.Name.Equals(simpleName + "`1", StringComparison.OrdinalIgnoreCase)); + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css index 575c6010..f33a08c6 100644 --- a/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css +++ b/SeeSharp.ReferenceManager/Pages/IntegratorSelector.razor.css @@ -11,23 +11,6 @@ gap: 0.5em; } -.integrator-header { - display: flex; - align-items: center; - padding: 4px 0; - cursor: pointer; - user-select: none; - background: transparent; - border: none; -} - -.integrator-content { - border: 1px solid #e0e0e0; - border-radius: 4px; - padding: 10px; - margin-top: 4px; -} - summary { cursor: pointer; font-weight: 600; @@ -60,45 +43,4 @@ details > div { details:hover > div { border-left-color: #ccc; } - - -.struct-details { - width: 100%; - margin-bottom: 2px; -} - -.struct-summary { - cursor: pointer; - list-style: none; - font-size: samller; - font-weight: normal; - color: #000; - background-color: #f5f5f5; - display: flex; - align-items: center; - transition: background-color 0.2s; -} -.struct-summary:hover { - background-color: #e9e9e9; -} -.struct-summary::before { - content: '▶'; - display: inline-block; - font-size: 0.7em; - margin-right: 8px; - color: #000; - transition: transform 0.2s ease; -} -.struct-details[open] > .struct-summary::before { - transform: rotate(90deg); -} - -.struct-content { - margin-left: 10px; - padding-left: 10px; - padding-top: 5px; - padding-bottom: 5px; - border-left: 2px solid #e0e0e0; -} - \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/IntegratorUtils.cs b/SeeSharp.ReferenceManager/Pages/IntegratorUtils.cs new file mode 100644 index 00000000..f40c2fb8 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/IntegratorUtils.cs @@ -0,0 +1,80 @@ +using System.Reflection; + +namespace SeeSharp.ReferenceManager.Pages; + +public class ParameterGroup { + public string Title { get; set; } = ""; + public List Properties { get; set; } = new(); + public List Fields { get; set; } = new(); + public bool HasParameters => Properties.Any() || Fields.Any(); +} + +public static class IntegratorUtils { + public static List GetParameterGroups(Integrator integrator) + { + var groups = new List(); + var currentType = integrator.GetType(); + + var allProps = GetFilteredProps(currentType); + var allFields = GetFilteredFields(currentType); + + while (currentType != null && currentType != typeof(object)) { + bool IsCurrentDeclared(MemberInfo m) + { + var d = m.DeclaringType; + var cur = currentType; + if (d != null && d.IsGenericType && !d.IsGenericTypeDefinition) d = d.GetGenericTypeDefinition(); + if (cur != null && cur.IsGenericType && !cur.IsGenericTypeDefinition) cur = cur.GetGenericTypeDefinition(); + return d == cur; + } + + string title = FormatClassName(currentType); + + bool isGlobalSettings = (currentType == typeof(Integrator)); + if (isGlobalSettings) title = "Global Settings"; + + var group = new ParameterGroup { + Title = title, + Properties = allProps.Where(p => IsCurrentDeclared(p)) + .Where(p => !isGlobalSettings || (p.Name != "MaxDepth" && p.Name != "MinDepth")).ToList(), + Fields = allFields.Where(f => IsCurrentDeclared(f)).ToList() + }; + + if (group.HasParameters) + groups.Add(group); + + currentType = currentType.BaseType; + } + + return groups; + } + + public static string FormatClassName(Type t) + { + string name = t.Name; + if (name.Contains('`')) name = name.Substring(0, name.IndexOf('`')); + return System.Text.RegularExpressions.Regex.Replace(name, "(\\B[A-Z])", " $1"); + } + + public static bool IsVisible(MemberInfo m) + { + if (m is PropertyInfo p && (!p.CanRead || !p.CanWrite)) return false; + if (m is FieldInfo f && (f.IsLiteral || f.IsInitOnly)) return false; + + Type t = (m is PropertyInfo pi) ? pi.PropertyType : ((FieldInfo)m).FieldType; + Type underlyingType = Nullable.GetUnderlyingType(t) ?? t; + + return underlyingType.IsPrimitive; + } + + public static IEnumerable GetFilteredProps(Type type) => + type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); + + public static IEnumerable GetFilteredFields(Type type) => + type.GetFields(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); + + public static string GetDescription(MemberInfo member) + { + return DocumentationReader.GetSummary(member) ?? ""; + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor index ec2cfcc3..c8bd96e3 100644 --- a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor @@ -26,7 +26,7 @@
- +
@@ -62,6 +62,11 @@ + +
+ + +
@@ -123,10 +128,61 @@ Iterations: @selectedFile.Spp Version: @selectedFile.Version + + @if (isVersionMismatch) + { +
+ Version Differs @ReferenceUtils.CurrentSeeSharpVersion +
+ } + else if (isVersionWarning) + { +
+ Version Differs @ReferenceUtils.CurrentSeeSharpVersion +
+ } + + @if (selectedFile.RenderSteps != null && selectedFile.RenderSteps.Count > 0) + { +
+
+ Render History +
+ @for (int i = 0; i < selectedFile.RenderSteps.Count; i++) + { + var step = selectedFile.RenderSteps[i]; +
+
+ @(i + 1) @step.Type +
+
+ RenderTime: @(step.DurationMs)ms
+ StartTime: @step.StartTime
+ WriteTime: @step.WriteTime +
+
+ } +
+
+
+ }
+ @if (isStructureMismatch) + { +
Parameters Mismatch
+ }
Integrator Settings + +
+ +
+
Integrator: @@ -139,23 +195,42 @@ if (node != null) { var ignoredKeys = new HashSet { - "RenderTime", "NumIterations", "RenderStartTime", "RenderWriteTime", - "SeeSharpVersion" + "RenderTime", "NumIterations", "RenderStartTime", "RenderWriteTime", + "SeeSharpVersion", "RenderSteps", "NaNWarnings" }; foreach (var kvp in node.AsObject()) { - if (ignoredKeys.Contains(kvp.Key)) continue; + bool isExtra = extraKeys.Contains(kvp.Key); + if (!isExtra && ignoredKeys.Contains(kvp.Key)) continue; + string valStr = kvp.Value?.ToString() ?? "null"; - string typeClass = "val"; + string typeClass = "val"; if (kvp.Value is System.Text.Json.Nodes.JsonValue jval) { if (jval.TryGetValue(out _)) typeClass = "bool"; else if (jval.TryGetValue(out _)) typeClass = "str"; } -
- @kvp.Key: + + string styleKey = isExtra ? "color: red; " : ""; + string titleAttr = isExtra ? "Parameter not found in current code" : ""; + +
+ @kvp.Key: @valStr
} + + if (missingKeys.Count > 0) + { +
+
Missing Parameters:
+ @foreach (var key in missingKeys) + { +
+ @key +
+ } +
+ } } } } @@ -170,669 +245,4 @@
-
- -@code { - class ReferenceInfo { - public string FilePath { get; set; } = ""; - public string Resolution { get; set; } = ""; - public int MaxDepth { get; set; } - public string IntegratorName { get; set; } = ""; - - public int Spp { get; set; } - public string RenderTimeDisplay { get; set; } = ""; - public string Version { get; set; } = ""; - public string StartTimeDisplay { get; set; } = ""; - public string WriteTimeDisplay { get; set; } = ""; - public string Timestamp { get; set; } = ""; - - public string RawJsonConfig { get; set; } = ""; - } - - SceneSelector sceneSelector; - IntegratorSelector integratorSelector; - Scene scene; - SceneFromFile currentSceneFile; - string currentSceneDirectory = ""; - FlipBook flip; - - List referenceFiles = new(); - ReferenceInfo selectedFile; - - int renderWidth = 512; - int renderHeight = 512; - int renderMaxDepth = 5; - int quickPreviewSpp = 1; - bool isRendering = false; - int additionalSpp = 32; - - void OnSceneLoaded(SceneFromFile sceneFromFile) - { - currentSceneFile = sceneFromFile; - scene = sceneFromFile.MakeScene(); - - if (!string.IsNullOrEmpty(sceneFromFile.Name)) - { - string sceneName = Path.GetFileNameWithoutExtension(sceneFromFile.Name); - var dir = new DirectoryInfo(AppContext.BaseDirectory); - while (dir != null && dir.Name != "SeeSharp") - dir = dir.Parent; - if (dir != null) - currentSceneDirectory = Path.Combine(dir.FullName, "Data", "Scenes", sceneName); - - ScanReferences(currentSceneDirectory); - } - } - - void ScanReferences(string sceneDir) - { - referenceFiles.Clear(); - if (string.IsNullOrEmpty(sceneDir)) return; - - string refDir = Path.Combine(sceneDir, "References"); - if (!Directory.Exists(refDir)) return; - - var exrFiles = Directory.GetFiles(refDir, "*.exr") - .OrderByDescending(f => File.GetLastWriteTime(f)); - - foreach (var f in exrFiles) { - var info = new ReferenceInfo { - FilePath = f, - Resolution = GetResolution(f), - Timestamp = File.GetLastWriteTime(f).ToString("yyyy-MM-dd HH:mm:ss") - }; - - ReadMetadataFromJson(info, f); - - referenceFiles.Add(info); - } - } - - void ReadMetadataFromJson(ReferenceInfo info, string exrPath) - { - string folder = Path.GetDirectoryName(exrPath); - string fileNameNoExt = Path.GetFileNameWithoutExtension(exrPath); - string jsonPath = Path.Combine(folder, $"{fileNameNoExt}.json"); - - if (!File.Exists(jsonPath)) return; - - string jsonContent = File.ReadAllText(jsonPath); - var root = JsonNode.Parse(jsonContent); - if (root == null) return; - - info.Version = root["SeeSharpVersion"]?.ToString() ?? ""; - info.StartTimeDisplay = root["RenderStartTime"]?.ToString() ?? ""; - info.WriteTimeDisplay = root["RenderWriteTime"]?.ToString() ?? ""; - - if (root["RenderTime"] != null) { - double ms = root["RenderTime"].GetValue(); - TimeSpan t = TimeSpan.FromMilliseconds(ms); - info.RenderTimeDisplay = t.TotalMinutes >= 1 ? $"{t.Minutes:D2}m {t.Seconds:D2}s" : $"{ms:F0}ms"; - } - - if (root["NumIterations"] != null) info.Spp = root["NumIterations"].GetValue(); - - var settingsNode = root["Settings"]; - if (settingsNode != null) { - var options = new JsonSerializerOptions { WriteIndented = true }; - info.RawJsonConfig = settingsNode.ToJsonString(options); - info.MaxDepth = settingsNode["MaxDepth"]?.GetValue() ?? 0; - } - - string integratorName = root["Name"]?.GetValue(); - info.IntegratorName = integratorName.Split('.').Last(); - } - - string GetResolution(string filePath) - { - string filename = Path.GetFileNameWithoutExtension(filePath); - var match = Regex.Match(filename, @"Width(\d+)-Height(\d+)", RegexOptions.IgnoreCase); - if (match.Success) return $"{match.Groups[1].Value}x{match.Groups[2].Value}"; - - return "Unknown"; - } - - void ResetConfig() - { - integratorSelector?.TriggerReset(); - } - - async Task ApplyConfig() - { - if (integratorSelector == null) return; - - string folder = Path.GetDirectoryName(selectedFile?.FilePath) ?? currentSceneDirectory ?? ""; - if (!folder.EndsWith("References")) { - string refDir = Path.Combine(folder, "References"); - if (Directory.Exists(refDir)) - folder = refDir; - } - string configPath = Path.Combine(folder, "Config.json"); - if (File.Exists(configPath)) { - if (LoadIntegratorFromJson(configPath)) - return; - } - } - - bool LoadIntegratorFromJson(string path) { - if (!File.Exists(path)) return false; - - string json = File.ReadAllText(path); - var root = JsonNode.Parse(json); - - JsonNode settingsNode = root["Settings"]; - - if (settingsNode == null) return false; - - var currentIntegrator = integratorSelector.addedIntegrators.FirstOrDefault(); - if (currentIntegrator == null) return false; - - var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; - var loaded = JsonSerializer.Deserialize(settingsNode, currentIntegrator.GetType(), options); - - CopyProperties(currentIntegrator, loaded); - StateHasChanged(); - return true; - } - - void CopyProperties(object target, object source) { - if (target == null || source == null || target.GetType() != source.GetType()) return; - var type = target.GetType(); - - bool IsConfigParam(Type t) { - return t == typeof(string) || t.IsValueType; - } - - foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite)) { - if (IsConfigParam(prop.PropertyType)) - prop.SetValue(target, prop.GetValue(source)); - } - foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { - if (IsConfigParam(field.FieldType)) - field.SetValue(target, field.GetValue(source)); - } - } - - void SelectReference(ReferenceInfo file) - { - selectedFile = file; - - string folder = Path.GetDirectoryName(file.FilePath); - string configPath = Path.Combine(folder, "Config.json"); - int configTargetSpp = 0; - - if (File.Exists(configPath)) { - var root = JsonNode.Parse(File.ReadAllText(configPath)); - var settings = root["Settings"]; - configTargetSpp = settings?["TotalSpp"]?.GetValue() - ?? settings?["NumIterations"]?.GetValue() - ?? 0; - } - - bool isPartial = file.FilePath.EndsWith("-partial.exr"); - if (isPartial && configTargetSpp > file.Spp) { - additionalSpp = configTargetSpp - file.Spp; - } - - if (File.Exists(file.FilePath)) { - UpdateViewerFromFile(file.FilePath); - } - } - - void UpdateViewerFromFile(string path) - { - flip = null; - StateHasChanged(); - - if (!File.Exists(path)) return; - var layers = SimpleImageIO.Layers.LoadFromFile(path); - var img = layers.Values.OfType().FirstOrDefault(); - - if (img != null) - UpdatePreviewFromMemory(img); - } - - async void UpdatePreviewFromMemory(RgbImage image) { - var imgClone = new RgbImage(image.Width, image.Height); - CopyImage(imgClone, image); - flip = null; - StateHasChanged(); - await Task.Delay(1); - var newFlip = new FlipBook(660, 580); - newFlip.Add("", imgClone); - flip = newFlip; - StateHasChanged(); - } - - void CopyImage(RgbImage target, RgbImage source) { - Parallel.For(0, target.Height, y => { - for (int x = 0; x < target.Width; ++x) - target.SetPixel(x, y, source.GetPixel(x, y)); - }); - } - - async Task RenderReference() - { - if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) return; - - isRendering = true; - flip = null; - await InvokeAsync(StateHasChanged); - - var curIntegrator = integratorSelector.addedIntegrators.First(); - - await Task.Run(async () => { - var startTime = DateTime.Now; - var stopwatch = Stopwatch.StartNew(); - - var renderScene = scene; - var renderIntegrator = CloneIntegrator(curIntegrator); - - SetMaxDepth(renderIntegrator, renderMaxDepth); - int targetSpp = GetTargetSpp(renderIntegrator); - - string referencesRoot = Path.Combine(currentSceneDirectory, "References"); - Directory.CreateDirectory(referencesRoot); - - SaveConfig(referencesRoot, renderIntegrator); - - string baseName = $"MaxDepth{renderMaxDepth}-Width{renderWidth}-Height{renderHeight}"; - string finalPath = Path.Combine(referencesRoot, baseName + ".exr"); - string partialPath = Path.Combine(referencesRoot, baseName + "-partial.exr"); - - var accumulator = new RgbImage(renderWidth, renderHeight); - renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); - renderScene.Prepare(); - - int currentSpp = 0; - int nextTargetSpp = 1; - - while (currentSpp < targetSpp) - { - int delta = nextTargetSpp - currentSpp; - if (currentSpp + delta > targetSpp) delta = targetSpp - currentSpp; - if (delta <= 0) break; - - SetBatchSpp(renderIntegrator, delta); - - renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); - renderIntegrator.Render(renderScene); - - var batchImg = new RgbImage(renderWidth, renderHeight); - CopyImage(batchImg, renderScene.FrameBuffer.Image); - batchImg.Scale((float)delta); - AddImage(accumulator, batchImg); - - currentSpp += delta; - - if (currentSpp == nextTargetSpp || currentSpp == targetSpp) { - SetBatchSpp(renderIntegrator, currentSpp); - WriteImageWithJson(accumulator, partialPath, renderIntegrator, currentSpp, renderMaxDepth, stopwatch.ElapsedMilliseconds, startTime); - - if (currentSpp < targetSpp) nextTargetSpp *= 2; - if (nextTargetSpp > targetSpp) nextTargetSpp = targetSpp; - } - } - stopwatch.Stop(); - - SetBatchSpp(renderIntegrator, targetSpp); - WriteImageWithJson(accumulator, finalPath, renderIntegrator, targetSpp, renderMaxDepth, stopwatch.ElapsedMilliseconds, startTime); - - if (File.Exists(partialPath)) File.Delete(partialPath); - string partialJson = Path.ChangeExtension(partialPath, ".json"); - if (File.Exists(partialJson)) File.Delete(partialJson); - - var newRef = new ReferenceInfo { - FilePath = finalPath, - Resolution = $"{renderWidth}x{renderHeight}", - MaxDepth = renderMaxDepth, - Spp = targetSpp, - Version = typeof(SeeSharp.Scene).Assembly.GetName().Version?.ToString() ?? "Unknown", - StartTimeDisplay = startTime.ToString("yyyy-MM-dd HH:mm:ss"), - WriteTimeDisplay = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), - Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), - IntegratorName = renderIntegrator.GetType().Name - }; - - double ms = stopwatch.ElapsedMilliseconds; - TimeSpan t = TimeSpan.FromMilliseconds(ms); - newRef.RenderTimeDisplay = t.TotalMinutes >= 1 ? $"{t.Minutes:D2}m {t.Seconds:D2}s" : $"{ms:F0}ms"; - - newRef.RawJsonConfig = JsonSerializer.Serialize(renderIntegrator, renderIntegrator.GetType(), new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); - - await InvokeAsync(() => { - var existing = referenceFiles.FirstOrDefault(r => r.FilePath == finalPath); - if (existing != null) referenceFiles.Remove(existing); - - referenceFiles.Insert(0, newRef); - selectedFile = newRef; - UpdateViewerFromFile(finalPath); - StateHasChanged(); - }); - }); - - isRendering = false; - await InvokeAsync(StateHasChanged); - } - - async Task RenderMoreSamples() - { - if (currentSceneFile == null || selectedFile == null || !File.Exists(selectedFile.FilePath)) return; - if (integratorSelector.addedIntegrators.Count == 0) return; - - isRendering = true; - await InvokeAsync(StateHasChanged); - var curIntegrator = integratorSelector.addedIntegrators.First(); - - await Task.Run(async () => { - long previousRenderTimeMs = 0; - DateTime originalStartTime = DateTime.Now; - uint originalBaseSeed = 0; - Integrator renderIntegrator = null; - - string folder = Path.GetDirectoryName(selectedFile.FilePath); - string fileNameNoExt = Path.GetFileNameWithoutExtension(selectedFile.FilePath); - string jsonPath = Path.Combine(folder, $"{fileNameNoExt}.json"); - - if (File.Exists(jsonPath)) { - try { - var root = JsonNode.Parse(File.ReadAllText(jsonPath)); - if (root != null) { - previousRenderTimeMs = (long)(root["RenderTime"]?.GetValue() ?? 0); - string startStr = root["RenderStartTime"]?.ToString(); - if (!DateTime.TryParse(startStr, out originalStartTime)) - DateTime.TryParseExact(startStr, "dd/M/yyyy HH:mm:ss", null, System.Globalization.DateTimeStyles.None, out originalStartTime); - - var settingsNode = root["Settings"]; - var uiType = integratorSelector.addedIntegrators.FirstOrDefault()?.GetType(); - if (settingsNode != null && uiType != null) { - var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; - renderIntegrator = JsonSerializer.Deserialize(settingsNode, uiType, options) as Integrator; - } - } - } catch {} - } - - if (renderIntegrator == null) { - string configPath = Path.Combine(folder, "Config.json"); - if (File.Exists(configPath)) { - var root = JsonNode.Parse(File.ReadAllText(configPath)); - var settingsNode = root["Settings"]; - var uiType = integratorSelector.addedIntegrators.FirstOrDefault()?.GetType(); - if (settingsNode != null && uiType != null) { - var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; - renderIntegrator = JsonSerializer.Deserialize(settingsNode, uiType, options) as Integrator; - } - } - } - - var stopwatch = Stopwatch.StartNew(); - - var oldImg = new RgbImage(selectedFile.FilePath); - int oldSpp = selectedFile.Spp; - int targetTotalSpp = oldSpp + additionalSpp; - - var accumulator = new RgbImage(oldImg.Width, oldImg.Height); - CopyImage(accumulator, oldImg); - accumulator.Scale((float)oldSpp); - - var renderScene = scene; - - originalBaseSeed = GetBaseSeed(renderIntegrator); - SetBaseSeed(renderIntegrator, originalBaseSeed + (uint)oldSpp); - - SetMaxDepth(renderIntegrator, selectedFile.MaxDepth); - - int currentSpp = oldSpp; - int nextTargetSpp = currentSpp + 1; - - renderScene.FrameBuffer = new FrameBuffer(oldImg.Width, oldImg.Height, ""); - renderScene.Prepare(); - - string currentPath = selectedFile.FilePath; - string finalPath, partialPath; - - if (currentPath.EndsWith("-partial.exr")) { - partialPath = currentPath; - finalPath = currentPath.Replace("-partial.exr", ".exr"); - } else { - finalPath = currentPath; - partialPath = Path.ChangeExtension(currentPath, "-partial.exr"); - } - - while (currentSpp < targetTotalSpp) - { - int delta = nextTargetSpp - currentSpp; - if (currentSpp + delta > targetTotalSpp) delta = targetTotalSpp - currentSpp; - if (delta <= 0) break; - - SetBatchSpp(renderIntegrator, delta); - renderScene.FrameBuffer = new FrameBuffer(oldImg.Width, oldImg.Height, ""); - renderIntegrator.Render(renderScene); - - var batchImg = new RgbImage(oldImg.Width, oldImg.Height); - CopyImage(batchImg, renderScene.FrameBuffer.Image); - batchImg.Scale((float)delta); - AddImage(accumulator, batchImg); - - currentSpp += delta; - long totalTimeNow = previousRenderTimeMs + stopwatch.ElapsedMilliseconds; - - SetBaseSeed(renderIntegrator, originalBaseSeed); - SetBatchSpp(renderIntegrator, currentSpp); - WriteImageWithJson(accumulator, partialPath, renderIntegrator, currentSpp, renderMaxDepth, totalTimeNow, originalStartTime); - - SetBaseSeed(renderIntegrator, originalBaseSeed + (uint)currentSpp); - - if (currentSpp < targetTotalSpp) nextTargetSpp += Math.Max(1, (nextTargetSpp - oldSpp) * 2); - if (nextTargetSpp > targetTotalSpp) nextTargetSpp = targetTotalSpp; - } - stopwatch.Stop(); - - long finalTotalTime = previousRenderTimeMs + stopwatch.ElapsedMilliseconds; - - SetBaseSeed(renderIntegrator, originalBaseSeed); - SetBatchSpp(renderIntegrator, targetTotalSpp); - WriteImageWithJson(accumulator, finalPath, renderIntegrator, targetTotalSpp, renderMaxDepth, finalTotalTime, originalStartTime); - - if (finalPath != partialPath) { - if (File.Exists(partialPath)) File.Delete(partialPath); - string pJson = Path.ChangeExtension(partialPath, ".json"); - if (File.Exists(pJson)) File.Delete(pJson); - } - - var newRef = new ReferenceInfo { FilePath = finalPath }; - ReadMetadataFromJson(newRef, finalPath); - - newRef.Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - newRef.StartTimeDisplay = originalStartTime.ToString("yyyy-MM-dd HH:mm:ss"); - newRef.WriteTimeDisplay = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - newRef.Spp = targetTotalSpp; - newRef.Resolution = $"{oldImg.Width}x{oldImg.Height}"; - - TimeSpan t = TimeSpan.FromMilliseconds(finalTotalTime); - newRef.RenderTimeDisplay = t.TotalMinutes >= 1 ? $"{t.Minutes:D2}m {t.Seconds:D2}s" : $"{finalTotalTime}ms"; - newRef.RawJsonConfig = JsonSerializer.Serialize(renderIntegrator, renderIntegrator.GetType(), new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }); - - await InvokeAsync(() => { - var existing = referenceFiles.FirstOrDefault(r => r.FilePath == currentPath); - if (existing != null) referenceFiles.Remove(existing); - - var existingFinal = referenceFiles.FirstOrDefault(r => r.FilePath == finalPath); - if (existingFinal != null) referenceFiles.Remove(existingFinal); - - referenceFiles.Insert(0, newRef); - selectedFile = newRef; - - UpdateViewerFromFile(finalPath); - StateHasChanged(); - }); - }); - - isRendering = false; - await InvokeAsync(StateHasChanged); - } - - async Task RenderQuickPreview() - { - if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) return; - - isRendering = true; - selectedFile = null; - flip = null; - - await InvokeAsync(StateHasChanged); - - var curIntegrator = integratorSelector.addedIntegrators.First(); - - await Task.Run(async () => { - var stopwatch = Stopwatch.StartNew(); - var renderScene = currentSceneFile.MakeScene(); - var renderIntegrator = CloneIntegrator(curIntegrator); - - int targetSpp = quickPreviewSpp < 1 ? 1 : quickPreviewSpp; - SetBatchSpp(renderIntegrator, targetSpp); - SetMaxDepth(renderIntegrator, renderMaxDepth); - - renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); - renderScene.Prepare(); - renderIntegrator.Render(renderScene); - - stopwatch.Stop(); - - var resultImg = new RgbImage(renderWidth, renderHeight); - CopyImage(resultImg, renderScene.FrameBuffer.Image); - - await InvokeAsync(() => { - UpdatePreviewFromMemory(resultImg); - }); - }); - - isRendering = false; - await InvokeAsync(StateHasChanged); - } - - Integrator CloneIntegrator(Integrator source) { - var type = source.GetType(); - var clone = (Integrator)Activator.CreateInstance(type); - bool IsConfigParam(Type t) { - return t == typeof(string) || t.IsValueType; - } - foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite)) - if (IsConfigParam(prop.PropertyType)) - prop.SetValue(clone, prop.GetValue(source)); - foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) - if (IsConfigParam(field.FieldType)) - field.SetValue(clone, field.GetValue(source)); - return clone; - } - - void SetMaxDepth(Integrator integrator, int depth) { - var type = integrator.GetType(); - var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - var prop = type.GetProperty("MaxDepth", flags); - if (prop != null && prop.CanWrite) { - prop.SetValue(integrator, depth); - return; - } - var field = type.GetField("MaxDepth", flags); - if (field != null) { - field.SetValue(integrator, depth); - } - } - - int GetTargetSpp(Integrator integrator) { - var type = integrator.GetType(); - var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - var propSpp = type.GetProperty("TotalSpp", flags); - if (propSpp != null) - return (int)propSpp.GetValue(integrator); - - var fieldSpp = type.GetField("TotalSpp", flags); - if (fieldSpp != null) - return (int)fieldSpp.GetValue(integrator); - - var propIter = type.GetProperty("NumIterations", flags); - if (propIter != null) - return (int)propIter.GetValue(integrator); - - var fieldIter = type.GetField("NumIterations", flags); - if (fieldIter != null) - return (int)fieldIter.GetValue(integrator); - return 16; - } - - void SaveConfig(string folder, Integrator integrator) { - var options = new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }; - var rootNode = new JsonObject(); - rootNode.Add("Name", integrator.GetType().Name); - var settingsNode = JsonSerializer.SerializeToNode(integrator, integrator.GetType(), options); - rootNode.Add("Settings", settingsNode); - File.WriteAllText(Path.Combine(folder, "Config.json"), rootNode.ToJsonString(options)); - } - - void SetBatchSpp(Integrator integrator, int batchCount) { - var type = integrator.GetType(); - var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - var targetNames = new[] { "TotalSpp", "NumIterations"}; - foreach (var name in targetNames) { - var prop = type.GetProperty(name, flags); - if (prop != null && prop.CanWrite) { - prop.SetValue(integrator, batchCount); - return; - } - var field = type.GetField(name, flags); - if (field != null) { - field.SetValue(integrator, batchCount); - return; - } - } - } - - void AddImage(RgbImage target, RgbImage source) { - Parallel.For(0, target.Height, y => { - for (int x = 0; x < target.Width; ++x) - target.SetPixel(x, y, target.GetPixel(x, y) + source.GetPixel(x, y)); - }); - } - - void WriteImageWithJson(RgbImage image, string filename, Integrator integrator, int spp, int depth, long timeMs, DateTime startTime) - { - var output = new RgbImage(image.Width, image.Height); - CopyImage(output, image); - if (spp > 0) output.Scale(1.0f / spp); - output.WriteToFile(filename); - - var rootNode = new JsonObject(); - rootNode.Add("RenderTime", timeMs); - rootNode.Add("NumIterations", spp); - rootNode.Add("RenderStartTime", startTime.ToString("yyyy-MM-dd HH:mm:ss")); - rootNode.Add("RenderWriteTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); - rootNode.Add("SeeSharpVersion", typeof(Scene).Assembly.GetName().Version?.ToString() ?? "Unknown"); - rootNode.Add("Name", integrator.GetType().Name); - - var options = new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }; - var settingsNode = JsonSerializer.SerializeToNode(integrator, integrator.GetType(), options); - rootNode.Add("Settings", settingsNode); - - string jsonPath = Path.ChangeExtension(filename, ".json"); - File.WriteAllText(jsonPath, rootNode.ToJsonString(options)); - } - - uint GetBaseSeed(Integrator integrator) { - var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - var seedProp = integrator.GetType().GetProperty("BaseSeed", flags); - var seedField = integrator.GetType().GetField("BaseSeed", flags); - if (seedProp != null) return (uint)(seedProp.GetValue(integrator) ?? 0u); - if (seedField != null) return (uint)(seedField.GetValue(integrator) ?? 0u); - return 0; - } - - void SetBaseSeed(Integrator integrator, uint seed) { - var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - var seedProp = integrator.GetType().GetProperty("BaseSeed", flags); - var seedField = integrator.GetType().GetField("BaseSeed", flags); - if (seedProp != null) seedProp.SetValue(integrator, seed); - else if (seedField != null) seedField.SetValue(integrator, seed); - } -} \ No newline at end of file +
\ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.cs b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.cs new file mode 100644 index 00000000..bc10160d --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.cs @@ -0,0 +1,406 @@ +using System.Text.Json.Nodes; + +namespace SeeSharp.ReferenceManager.Pages; + +public partial class ReferenceRendering +{ + private SceneSelector sceneSelector; + private IntegratorSelector integratorSelector; + private Scene scene; + private SceneFromFile currentSceneFile; + private string currentSceneDirectory = ""; + private FlipBook flip; + + private List referenceFiles = new(); + private ReferenceInfo selectedFile; + + private int renderWidth = 512; + private int renderHeight = 512; + private int renderMaxDepth = 5; + private int renderMinDepth = 1; + private int quickPreviewSpp = 1; + private bool isRendering = false; + private int additionalSpp = 32; + private bool isStructureMismatch = false; + private bool isVersionMismatch = false; + private bool isVersionWarning = false; + private HashSet extraKeys = new(); + private HashSet missingKeys = new(); + + private void OnSceneLoaded(SceneFromFile sceneFromFile) + { + currentSceneFile = sceneFromFile; + scene = sceneFromFile.MakeScene(); + + if (!string.IsNullOrEmpty(sceneFromFile.Name)) + { + string sceneName = Path.GetFileNameWithoutExtension(sceneFromFile.Name); + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null && dir.Name != "SeeSharp") + dir = dir.Parent; + if (dir != null) + currentSceneDirectory = Path.Combine(dir.FullName, "Data", "Scenes", sceneName); + + ReferenceUtils.ScanReferences(currentSceneDirectory, referenceFiles); + } + } + + private void ResetConfig() => integratorSelector?.TriggerReset(); + + private void SaveSceneConfig() + { + if (integratorSelector?.CurrentIntegrator == null) return; + string folder = Path.Combine(currentSceneDirectory, "References"); + if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); + ReferenceUtils.SaveConfig(folder, integratorSelector.CurrentIntegrator); + StateHasChanged(); + } + + private void ApplyReferenceSettings() + { + if (selectedFile == null || string.IsNullOrEmpty(selectedFile.RawJsonConfig)) return; + if (integratorSelector == null) return; + + if (!string.IsNullOrEmpty(selectedFile.IntegratorName)) + { + bool success = integratorSelector.TrySelectIntegrator(selectedFile.IntegratorName); + } + + var currentIntegrator = integratorSelector.CurrentIntegrator; + if (currentIntegrator == null) return; + + var node = JsonNode.Parse(selectedFile.RawJsonConfig); + if (node != null) { + var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; + var loadedIntegrator = JsonSerializer.Deserialize(node, currentIntegrator.GetType(), options); + + if (loadedIntegrator != null) { + ReferenceUtils.CopyValues(currentIntegrator, loadedIntegrator); + if (selectedFile.MaxDepth > 0) renderMaxDepth = selectedFile.MaxDepth; + if (selectedFile.MinDepth > 0) renderMinDepth = selectedFile.MinDepth; + StateHasChanged(); + } + } + } + + private void SelectReference(ReferenceInfo file) + { + selectedFile = file; + if (File.Exists(file.FilePath)) + UpdateViewerFromFile(file.FilePath); + + CheckParamsMatch(); + } + + private void UpdateViewerFromFile(string path) + { + flip = null; + StateHasChanged(); + + if (!File.Exists(path)) return; + var layers = SimpleImageIO.Layers.LoadFromFile(path); + var img = layers.Values.OfType().FirstOrDefault(); + + if (img != null) + UpdatePreviewFromMemory(img); + } + + private async void UpdatePreviewFromMemory(RgbImage image) { + var imgClone = new RgbImage(image.Width, image.Height); + ReferenceUtils.CopyImage(imgClone, image); + flip = null; + StateHasChanged(); + await Task.Delay(1); + var newFlip = new FlipBook(660, 580); + newFlip.Add("", imgClone); + flip = newFlip; + StateHasChanged(); + } + + async Task RenderQuickPreview() + { + if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) return; + + isRendering = true; + selectedFile = null; + flip = null; + + await InvokeAsync(StateHasChanged); + + var curIntegrator = integratorSelector.addedIntegrators.First(); + + await Task.Run(async () => { + var stopwatch = Stopwatch.StartNew(); + var renderScene = currentSceneFile.MakeScene(); + var renderIntegrator = ReferenceUtils.CloneIntegrator(curIntegrator); + + int targetSpp = quickPreviewSpp < 1 ? 1 : quickPreviewSpp; + ReferenceUtils.SetBatchSpp(renderIntegrator, targetSpp); + ReferenceUtils.SetMaxDepth(renderIntegrator, renderMaxDepth); + ReferenceUtils.SetMinDepth(renderIntegrator, renderMinDepth); + + renderScene.FrameBuffer = new FrameBuffer(renderWidth, renderHeight, ""); + renderScene.Prepare(); + renderIntegrator.Render(renderScene); + + stopwatch.Stop(); + + var resultImg = new RgbImage(renderWidth, renderHeight); + ReferenceUtils.CopyImage(resultImg, renderScene.FrameBuffer.Image); + + await InvokeAsync(() => { + UpdatePreviewFromMemory(resultImg); + }); + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + async Task RenderReference() + { + if (currentSceneFile == null || integratorSelector.addedIntegrators.Count == 0) return; + + isRendering = true; + flip = null; + await InvokeAsync(StateHasChanged); + + var curIntegrator = integratorSelector.addedIntegrators.First(); + + await Task.Run(async () => { + var renderIntegrator = ReferenceUtils.CloneIntegrator(curIntegrator); + + string referencesRoot = Path.Combine(currentSceneDirectory, "References"); + Directory.CreateDirectory(referencesRoot); + + ReferenceUtils.SetMaxDepth(renderIntegrator, renderMaxDepth); + ReferenceUtils.SetMinDepth(renderIntegrator, renderMinDepth); + ReferenceUtils.SaveConfig(referencesRoot, renderIntegrator); + + string baseName = $"MaxDepth{renderMaxDepth}-Width{renderWidth}-Height{renderHeight}"; + string finalPath = Path.Combine(referencesRoot, baseName + ".exr"); + string partialPath = Path.Combine(referencesRoot, baseName + "-partial.exr"); + + var fbFlags = FrameBuffer.Flags.WriteContinously | FrameBuffer.Flags.WriteExponentially | FrameBuffer.Flags.IgnoreNanAndInf; + + ReferenceUtils.PrepareFrameBuffer(scene, renderWidth, renderHeight, partialPath, renderIntegrator, fbFlags); + scene.Prepare(); + + int targetSpp = ReferenceUtils.GetTargetSpp(renderIntegrator); + ReferenceUtils.SetBatchSpp(renderIntegrator, targetSpp); + + renderIntegrator.Render(scene); + scene.FrameBuffer.WriteToFile(); + + int actualSpp = scene.FrameBuffer.CurIteration; + bool isCompleted = actualSpp >= targetSpp; + string finalPathToLoad = partialPath; + + if (isCompleted) + { + if (File.Exists(finalPath)) File.Delete(finalPath); + if (File.Exists(partialPath)) File.Move(partialPath, finalPath); + + string pJson = Path.ChangeExtension(partialPath, ".json"); + string fJson = Path.ChangeExtension(finalPath, ".json"); + + if (File.Exists(fJson)) File.Delete(fJson); + if (File.Exists(pJson)) File.Move(pJson, fJson); + + finalPathToLoad = finalPath; + } + + await InvokeAsync(() => { + ReferenceUtils.ScanReferences(currentSceneDirectory, referenceFiles); + selectedFile = referenceFiles.FirstOrDefault(r => r.FilePath == finalPathToLoad); + if (selectedFile != null) UpdateViewerFromFile(finalPathToLoad); + StateHasChanged(); + }); + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + async Task RenderMoreSamples(bool isResume) + { + if (currentSceneFile == null || selectedFile == null || !File.Exists(selectedFile.FilePath)) return; + if (integratorSelector.addedIntegrators.Count == 0) return; + + isRendering = true; + await InvokeAsync(StateHasChanged); + + await Task.Run(async () => { + try { + string filePath = selectedFile.FilePath; + var oldImg = new RgbImage(filePath); + int currentSpp = selectedFile.Spp; + + string folder = Path.GetDirectoryName(filePath); + string baseNameNoSuffix = Path.GetFileNameWithoutExtension(filePath).Replace("-partial", ""); + string jsonPath = Path.Combine(folder, baseNameNoSuffix + (filePath.Contains("-partial") ? "-partial.json" : ".json")); + + string thisStepStart = DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"); + JsonNode rootNode = null; + Integrator renderIntegrator = null; + long totalPreviousMs = 0; + int settingsTargetSpp = 0; + + if (File.Exists(jsonPath)) { + rootNode = JsonNode.Parse(File.ReadAllText(jsonPath)); + totalPreviousMs = (long)(rootNode?["RenderTime"]?.GetValue() ?? 0); + var settingsNode = rootNode?["Settings"]; + if (settingsNode != null) { + settingsTargetSpp = settingsNode["TotalSpp"]?.GetValue() ?? settingsNode["NumIterations"]?.GetValue() ?? 0; + Type targetType = integratorSelector.GetIntegratorType(rootNode["Name"]?.GetValue()); + if (targetType != null) { + var options = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; + renderIntegrator = JsonSerializer.Deserialize(settingsNode, targetType, options) as Integrator; + } + } + } + + int batchSpp = isResume ? (settingsTargetSpp - currentSpp) : additionalSpp; + int finalTotalSpp = currentSpp + batchSpp; + if (batchSpp <= 0) return; + + ReferenceUtils.SetBatchSpp(renderIntegrator, batchSpp); + if (!isResume) + { + uint originalBaseSeed = ReferenceUtils.GetBaseSeed(renderIntegrator); + ReferenceUtils.SetBaseSeed(renderIntegrator, originalBaseSeed + (uint)currentSpp); + } + + string currentPartialPath = Path.Combine(folder, baseNameNoSuffix + "-partial.exr"); + var fbFlags = FrameBuffer.Flags.WriteContinously | FrameBuffer.Flags.WriteExponentially | FrameBuffer.Flags.IgnoreNanAndInf; + + scene.FrameBuffer = new FrameBuffer(oldImg.Width, oldImg.Height, currentPartialPath, fbFlags); + scene.Prepare(); + + var stopwatch = Stopwatch.StartNew(); + renderIntegrator.Render(scene); + stopwatch.Stop(); + + long thisStepMs = stopwatch.ElapsedMilliseconds; + string thisStepWrite = DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"); + + var newImg = scene.FrameBuffer.Image; + var finalImg = new RgbImage(oldImg.Width, oldImg.Height); + float wOld = (float)currentSpp / finalTotalSpp; + float wNew = (float)batchSpp / finalTotalSpp; + + Parallel.For(0, finalImg.Height, row => { + for (int col = 0; col < finalImg.Width; ++col) { + finalImg.SetPixel(col, row, oldImg.GetPixel(col, row) * wOld + newImg.GetPixel(col, row) * wNew); + } + }); + + if (rootNode == null) rootNode = new JsonObject(); + rootNode["RenderTime"] = totalPreviousMs + thisStepMs; + rootNode["RenderWriteTime"] = thisStepWrite; + rootNode["NumIterations"] = finalTotalSpp; + + var steps = rootNode["RenderSteps"]?.AsArray() ?? new JsonArray(); + steps.Add(new JsonObject { + ["Type"] = isResume ? "Resume" : "More", + ["DurationMs"] = thisStepMs, + ["StartTime"] = thisStepStart, + ["WriteTime"] = thisStepWrite + }); + rootNode["RenderSteps"] = steps; + + if (!isResume && rootNode["Settings"] != null) { + rootNode["Settings"]["TotalSpp"] = finalTotalSpp; + } + + string finalSavePath = isResume ? Path.Combine(folder, baseNameNoSuffix + ".exr") : filePath; + finalImg.WriteToFile(finalSavePath); + File.WriteAllText(Path.ChangeExtension(finalSavePath, ".json"), rootNode.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); + + if (File.Exists(currentPartialPath)) File.Delete(currentPartialPath); + string partialJsonPath = Path.ChangeExtension(currentPartialPath, ".json"); + if (File.Exists(partialJsonPath)) File.Delete(partialJsonPath); + + if (isResume && filePath != finalSavePath) { + if (File.Exists(filePath)) File.Delete(filePath); + string oldJ = Path.ChangeExtension(filePath, ".json"); + if (File.Exists(oldJ)) File.Delete(oldJ); + } + + await InvokeAsync(() => { + ReferenceUtils.ScanReferences(currentSceneDirectory, referenceFiles); + selectedFile = referenceFiles.FirstOrDefault(r => r.FilePath == finalSavePath); + if (selectedFile != null) UpdateViewerFromFile(finalSavePath); + StateHasChanged(); + }); + } catch (Exception ex) { Console.WriteLine(ex.ToString()); } + }); + + isRendering = false; + await InvokeAsync(StateHasChanged); + } + + private void CheckParamsMatch() + { + isStructureMismatch = false; + isVersionMismatch = false; + isVersionWarning = false; + extraKeys.Clear(); + missingKeys.Clear(); + + if (selectedFile == null) return; + if (string.IsNullOrEmpty(selectedFile.RawJsonConfig)) return; + + (int Major, int Minor, int Patch) ParseSemVer(string ver) + { + if (string.IsNullOrEmpty(ver)) return (0, 0, 0); + int idx = ver.IndexOfAny(new[] { '+', '-' }); + if (idx >= 0) ver = ver.Substring(0, idx); + + var parts = ver.Split('.'); + int maj = parts.Length > 0 && int.TryParse(parts[0], out int m) ? m : 0; + int min = parts.Length > 1 && int.TryParse(parts[1], out int n) ? n : 0; + int pat = parts.Length > 2 && int.TryParse(parts[2], out int p) ? p : 0; + + return (maj, min, pat); + } + + string currentVerStr = ReferenceUtils.CurrentSeeSharpVersion; + string fileVerStr = selectedFile.Version; + + var cur = ParseSemVer(currentVerStr); + var file = ParseSemVer(fileVerStr); + + if (cur.Major != file.Major || cur.Minor != file.Minor) isVersionMismatch = true; + else if (cur.Patch != file.Patch) isVersionWarning = true; + + Type targetType = integratorSelector?.GetIntegratorType(selectedFile.IntegratorName); + if (targetType == null) { + isStructureMismatch = true; + StateHasChanged(); + return; + } + + object dummyIntegrator = Activator.CreateInstance(targetType); + var options = new JsonSerializerOptions { IncludeFields = true }; + var codeJson = JsonSerializer.Serialize(dummyIntegrator, targetType, options); + var codeNode = JsonNode.Parse(codeJson)?.AsObject(); + var fileNode = JsonNode.Parse(selectedFile.RawJsonConfig)?.AsObject(); + + if (codeNode != null && fileNode != null) + { + var codeKeys = codeNode.Select(k => k.Key).ToHashSet(); + var fileKeys = fileNode.Select(k => k.Key).ToHashSet(); + + foreach (var key in fileKeys) { + if (!codeKeys.Contains(key)) extraKeys.Add(key); + } + foreach (var key in codeKeys) { + if (!fileKeys.Contains(key)) missingKeys.Add(key); + } + if (extraKeys.Count > 0 || missingKeys.Count > 0) isStructureMismatch = true; + } + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css index 3585df77..eb04fdbb 100644 --- a/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css +++ b/SeeSharp.ReferenceManager/Pages/ReferenceRendering.razor.css @@ -36,13 +36,6 @@ padding: 10px; } -.btn-success-anim { - background-color: #28a745 !important; - border-color: #28a745 !important; - color: white !important; - transition: all 0.1s ease; -} - .ref-item { padding: 1px; border: 1px solid #eee; @@ -95,15 +88,6 @@ overflow: hidden; } -.slider-area { - flex: 0 0 auto; - padding: 5px 10px; - background: #f1f1f1; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - z-index: 5; -} - .meta-box { height: auto; min-height: 100px; diff --git a/SeeSharp.ReferenceManager/Pages/ReferenceUtils.cs b/SeeSharp.ReferenceManager/Pages/ReferenceUtils.cs new file mode 100644 index 00000000..621fa5a2 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/ReferenceUtils.cs @@ -0,0 +1,249 @@ +using System.Reflection; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace SeeSharp.ReferenceManager.Pages; + +public class ReferenceInfo { + public string FilePath { get; set; } = ""; + public string Resolution { get; set; } = ""; + public int MaxDepth { get; set; } + public int MinDepth { get; set; } + public string IntegratorName { get; set; } = ""; + public int Spp { get; set; } + public string RenderTimeDisplay { get; set; } = ""; + public string Version { get; set; } = ""; + public string StartTimeDisplay { get; set; } = ""; + public string WriteTimeDisplay { get; set; } = ""; + public string Timestamp { get; set; } = ""; + public string RawJsonConfig { get; set; } = ""; + public List RenderSteps { get; set; } = new(); +} + +public class RenderStep { + public string Type { get; set; } = ""; + public double DurationMs { get; set; } + public string StartTime { get; set; } = ""; + public string WriteTime { get; set; } = ""; +} + +public static class ReferenceUtils { + public static void ScanReferences(string sceneDir, List referenceFiles) { + referenceFiles.Clear(); + if (string.IsNullOrEmpty(sceneDir)) return; + + string refDir = Path.Combine(sceneDir, "References"); + if (!Directory.Exists(refDir)) return; + + var exrFiles = Directory.GetFiles(refDir, "*.exr") + .OrderByDescending(f => File.GetLastWriteTime(f)); + + foreach (var f in exrFiles) { + var info = new ReferenceInfo { + FilePath = f, + Resolution = GetResolution(f), + Timestamp = File.GetLastWriteTime(f).ToString("yyyy-MM-dd HH:mm:ss") + }; + ReadMetadataFromJson(info, f); + referenceFiles.Add(info); + } + } + + public static void ReadMetadataFromJson(ReferenceInfo info, string exrPath) { + string folder = Path.GetDirectoryName(exrPath); + string fileNameNoExt = Path.GetFileNameWithoutExtension(exrPath); + string jsonPath = Path.Combine(folder, $"{fileNameNoExt}.json"); + + if (!File.Exists(jsonPath)) return; + + string jsonContent = File.ReadAllText(jsonPath); + var root = JsonNode.Parse(jsonContent); + if (root == null) return; + + info.Version = root["SeeSharpVersion"]?.ToString() ?? ""; + info.StartTimeDisplay = root["RenderStartTime"]?.ToString() ?? ""; + info.WriteTimeDisplay = root["RenderWriteTime"]?.ToString() ?? ""; + + if (root["RenderTime"] != null) { + double ms = root["RenderTime"].GetValue(); + TimeSpan t = TimeSpan.FromMilliseconds(ms); + if (t.TotalMinutes >= 1) info.RenderTimeDisplay = $"{(int)t.TotalMinutes:D2} m {t.Seconds:D2} s"; + else if (t.TotalSeconds >= 1) info.RenderTimeDisplay = $"{t.TotalSeconds:F1} s"; + else info.RenderTimeDisplay = $"{ms:F0} ms"; + } + + if (root["NumIterations"] != null) info.Spp = root["NumIterations"].GetValue(); + + var stepsNode = root["RenderSteps"]; + if (stepsNode is JsonArray arr) { + foreach (var step in arr) { + if (step == null) continue; + info.RenderSteps.Add(new RenderStep { + Type = step["Type"]?.ToString() ?? "Unknown", + DurationMs = step["DurationMs"]?.GetValue() ?? 0, + StartTime = step["StartTime"]?.ToString() ?? "", + WriteTime = step["WriteTime"]?.ToString() ?? "" + }); + } + } + + var settingsNode = root["Settings"]; + if (settingsNode != null) { + var options = new JsonSerializerOptions { WriteIndented = true }; + info.RawJsonConfig = settingsNode.ToJsonString(options); + info.MaxDepth = settingsNode["MaxDepth"]?.GetValue() ?? 0; + info.MinDepth = settingsNode["MinDepth"]?.GetValue() ?? 0; + } + + string integratorName = root["Name"]?.GetValue(); + info.IntegratorName = integratorName.Split('.').Last(); + } + + public static string GetResolution(string filePath) { + string filename = Path.GetFileNameWithoutExtension(filePath); + var match = Regex.Match(filename, @"Width(\d+)-Height(\d+)", RegexOptions.IgnoreCase); + if (match.Success) return $"{match.Groups[1].Value}x{match.Groups[2].Value}"; + return "Unknown"; + } + + public static void CopyValues(object target, object source) { + if (target == null || source == null || target.GetType() != source.GetType()) return; + var type = target.GetType(); + + bool IsConfigParam(Type t) { + return t == typeof(string) || t.IsValueType; + } + + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead && p.CanWrite)) { + if (IsConfigParam(prop.PropertyType)) + prop.SetValue(target, prop.GetValue(source)); + } + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { + if (IsConfigParam(field.FieldType)) + field.SetValue(target, field.GetValue(source)); + } + } + + public static void CopyImage(RgbImage target, RgbImage source) { + Parallel.For(0, target.Height, y => { + for (int x = 0; x < target.Width; ++x) + target.SetPixel(x, y, source.GetPixel(x, y)); + }); + } + + public static Integrator CloneIntegrator(Integrator source) { + var type = source.GetType(); + var clone = (Integrator)Activator.CreateInstance(type); + CopyValues(clone, source); + return clone; + } + + public static void SetMaxDepth(Integrator integrator, int depth) { + var type = integrator.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var prop = type.GetProperty("MaxDepth", flags); + if (prop != null && prop.CanWrite) { + prop.SetValue(integrator, depth); + return; + } + var field = type.GetField("MaxDepth", flags); + if (field != null) { + field.SetValue(integrator, depth); + } + } + + public static void SetMinDepth(Integrator integrator, int depth) { + var type = integrator.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var prop = type.GetProperty("MinDepth", flags); + if (prop != null && prop.CanWrite) { + prop.SetValue(integrator, depth); + return; + } + var field = type.GetField("MinDepth", flags); + if (field != null) { + field.SetValue(integrator, depth); + } + } + + public static int GetTargetSpp(Integrator integrator) { + var type = integrator.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var propSpp = type.GetProperty("TotalSpp", flags); + if (propSpp != null) return (int)propSpp.GetValue(integrator); + + var fieldSpp = type.GetField("TotalSpp", flags); + if (fieldSpp != null) return (int)fieldSpp.GetValue(integrator); + + var propIter = type.GetProperty("NumIterations", flags); + if (propIter != null) return (int)propIter.GetValue(integrator); + + var fieldIter = type.GetField("NumIterations", flags); + if (fieldIter != null) return (int)fieldIter.GetValue(integrator); + return 16; + } + + public static void SaveConfig(string folder, Integrator integrator) { + var options = new JsonSerializerOptions { WriteIndented = true, IncludeFields = true }; + var rootNode = new JsonObject(); + rootNode.Add("Name", integrator.GetType().Name); + var settingsNode = JsonSerializer.SerializeToNode(integrator, integrator.GetType(), options); + rootNode.Add("Settings", settingsNode); + File.WriteAllText(Path.Combine(folder, "Config.json"), rootNode.ToJsonString(options)); + } + + public static void SetBatchSpp(Integrator integrator, int batchCount) { + var type = integrator.GetType(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var targetNames = new[] { "TotalSpp", "NumIterations"}; + foreach (var name in targetNames) { + var prop = type.GetProperty(name, flags); + if (prop != null && prop.CanWrite) { + prop.SetValue(integrator, batchCount); + return; + } + var field = type.GetField(name, flags); + if (field != null) { + field.SetValue(integrator, batchCount); + return; + } + } + } + + public static uint GetBaseSeed(Integrator integrator) { + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var seedProp = integrator.GetType().GetProperty("BaseSeed", flags); + var seedField = integrator.GetType().GetField("BaseSeed", flags); + if (seedProp != null) return (uint)(seedProp.GetValue(integrator) ?? 0u); + if (seedField != null) return (uint)(seedField.GetValue(integrator) ?? 0u); + return 0; + } + + public static void SetBaseSeed(Integrator integrator, uint seed) { + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + var seedProp = integrator.GetType().GetProperty("BaseSeed", flags); + var seedField = integrator.GetType().GetField("BaseSeed", flags); + if (seedProp != null) seedProp.SetValue(integrator, seed); + else if (seedField != null) seedField.SetValue(integrator, seed); + } + + public static void PrepareFrameBuffer(Scene scene, int width, int height, string finalPath, Integrator integrator, FrameBuffer.Flags flags) { + if (scene == null) return; + + scene.FrameBuffer = new FrameBuffer(width, height, finalPath, flags); + + var options = new JsonSerializerOptions { + IncludeFields = true, + WriteIndented = true + }; + var fullSettingsNode = JsonSerializer.SerializeToNode(integrator, integrator.GetType(), options); + + scene.FrameBuffer.MetaData["Name"] = integrator.GetType().Name; + scene.FrameBuffer.MetaData["Settings"] = fullSettingsNode; + } + + public static string CurrentSeeSharpVersion { get; } = + typeof(Scene).Assembly + .GetCustomAttribute()? + .InformationalVersion ?? "Unknown"; +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/RenderSetting.razor b/SeeSharp.ReferenceManager/Pages/RenderSetting.razor new file mode 100644 index 00000000..30d84471 --- /dev/null +++ b/SeeSharp.ReferenceManager/Pages/RenderSetting.razor @@ -0,0 +1,41 @@ +@using System.Reflection +@using SeeSharp.Blazor +@using SeeSharp.ReferenceManager.Pages + +@{ + string name = Member.Name; + string desc = IntegratorUtils.GetDescription(Member); + + Type type = (Member is PropertyInfo p) ? p.PropertyType : ((FieldInfo)Member).FieldType; + Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; + TypeCode typeCode = Type.GetTypeCode(underlyingType); + + if (typeCode == TypeCode.Boolean) + { + bool val = Convert.ToBoolean(Getter() ?? false); + + } + else if (typeCode == TypeCode.Single || typeCode == TypeCode.Double) + { + float val = (float)Convert.ToDouble(Getter() ?? 0.0); + + } + else if (typeCode >= TypeCode.SByte && typeCode <= TypeCode.UInt64) + { + long val = Convert.ToInt64(Getter() ?? 0); + + } +} + +@code { + [Parameter] public MemberInfo Member { get; set; } = default!; + [Parameter] public Func Getter { get; set; } = default!; + [Parameter] public Action Setter { get; set; } = default!; +} \ No newline at end of file diff --git a/SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs b/SeeSharp/Common/DocumentationReader.cs similarity index 78% rename from SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs rename to SeeSharp/Common/DocumentationReader.cs index ed2449bd..39cf6abd 100644 --- a/SeeSharp.ReferenceManager/Pages/DocumentationHelper.cs +++ b/SeeSharp/Common/DocumentationReader.cs @@ -1,16 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Xml.Linq; namespace SeeSharp.Blazor; -public static class DocumentationHelper +/// +/// Load xml documentation files and retrieve summary to show description +/// +public static class DocumentationReader { - private static Dictionary _loadedXmlDocumentation = new(); + private static Dictionary loadedXmlDocumentation = new(); public static void LoadXmlDocumentation(Assembly assembly) { @@ -30,13 +29,13 @@ public static void LoadXmlDocumentation(Assembly assembly) if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(summary)) { string cleanSummary = Regex.Replace(summary, @"\s+", " "); - _loadedXmlDocumentation[name] = cleanSummary; + loadedXmlDocumentation[name] = cleanSummary; } } } catch (Exception ex) { - Console.WriteLine($"[DocHelper] Error loading XML: {ex.Message}"); + Logger.Log($"Error loading XML: {ex.Message}", Verbosity.Error); } } } @@ -61,7 +60,7 @@ public static string GetSummary(MemberInfo member) string key = $"{prefix}{typeName}.{member.Name}"; - if (_loadedXmlDocumentation.TryGetValue(key, out var summary)) + if (loadedXmlDocumentation.TryGetValue(key, out var summary)) { return summary; }