From 2e4253896304b8949b03facff10ded3846d0e357 Mon Sep 17 00:00:00 2001 From: mzwang34 <“mzwang3434@gmail.com”> Date: Fri, 20 Feb 2026 15:39:53 +0100 Subject: [PATCH 1/4] init experiment configuration UI --- ExperimentConfigTest/App.razor | 12 + .../ExperimentConfigTest.csproj | 17 + ExperimentConfigTest/Imports.cs | 35 ++ ExperimentConfigTest/MainLayout.razor | 3 + ExperimentConfigTest/Pages/Experiment.razor | 108 +++++ .../Pages/Experiment.razor.cs | 54 +++ ExperimentConfigTest/Pages/Index.razor | 39 ++ ExperimentConfigTest/Pages/_Host.cshtml | 34 ++ ExperimentConfigTest/Program.cs | 27 ++ .../Properties/launchSettings.json | 35 ++ ExperimentConfigTest/_Imports.razor | 4 + .../appsettings.Development.json | 9 + ExperimentConfigTest/appsettings.json | 9 + ExperimentConfigTest/wwwroot/css/site.css | 86 ++++ SeeSharp.Blazor/IntegratorSelector.razor | 135 ++++++ SeeSharp.Blazor/IntegratorSelector.razor.cs | 406 ++++++++++++++++++ SeeSharp.Blazor/IntegratorSelector.razor.css | 180 ++++++++ SeeSharp.Blazor/IntegratorUtils.cs | 96 +++++ SeeSharp.Blazor/RenderSetting.razor | 41 ++ 19 files changed, 1330 insertions(+) create mode 100644 ExperimentConfigTest/App.razor create mode 100644 ExperimentConfigTest/ExperimentConfigTest.csproj create mode 100644 ExperimentConfigTest/Imports.cs create mode 100644 ExperimentConfigTest/MainLayout.razor create mode 100644 ExperimentConfigTest/Pages/Experiment.razor create mode 100644 ExperimentConfigTest/Pages/Experiment.razor.cs create mode 100644 ExperimentConfigTest/Pages/Index.razor create mode 100644 ExperimentConfigTest/Pages/_Host.cshtml create mode 100644 ExperimentConfigTest/Program.cs create mode 100644 ExperimentConfigTest/Properties/launchSettings.json create mode 100644 ExperimentConfigTest/_Imports.razor create mode 100644 ExperimentConfigTest/appsettings.Development.json create mode 100644 ExperimentConfigTest/appsettings.json create mode 100644 ExperimentConfigTest/wwwroot/css/site.css create mode 100644 SeeSharp.Blazor/IntegratorSelector.razor create mode 100644 SeeSharp.Blazor/IntegratorSelector.razor.cs create mode 100644 SeeSharp.Blazor/IntegratorSelector.razor.css create mode 100644 SeeSharp.Blazor/IntegratorUtils.cs create mode 100644 SeeSharp.Blazor/RenderSetting.razor diff --git a/ExperimentConfigTest/App.razor b/ExperimentConfigTest/App.razor new file mode 100644 index 00000000..6fd3ed1b --- /dev/null +++ b/ExperimentConfigTest/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/ExperimentConfigTest/ExperimentConfigTest.csproj b/ExperimentConfigTest/ExperimentConfigTest.csproj new file mode 100644 index 00000000..040d56b9 --- /dev/null +++ b/ExperimentConfigTest/ExperimentConfigTest.csproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + enable + + + + + + + + + + + diff --git a/ExperimentConfigTest/Imports.cs b/ExperimentConfigTest/Imports.cs new file mode 100644 index 00000000..447fdbcd --- /dev/null +++ b/ExperimentConfigTest/Imports.cs @@ -0,0 +1,35 @@ +#pragma warning disable CS8019 + +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.SceneManagement; +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/ExperimentConfigTest/MainLayout.razor b/ExperimentConfigTest/MainLayout.razor new file mode 100644 index 00000000..a5af3489 --- /dev/null +++ b/ExperimentConfigTest/MainLayout.razor @@ -0,0 +1,3 @@ +@inherits LayoutComponentBase + +
@Body
diff --git a/ExperimentConfigTest/Pages/Experiment.razor b/ExperimentConfigTest/Pages/Experiment.razor new file mode 100644 index 00000000..8f1b9d30 --- /dev/null +++ b/ExperimentConfigTest/Pages/Experiment.razor @@ -0,0 +1,108 @@ +@using SeeSharp.Experiments +@using SeeSharp +@using SeeSharp.Blazor + +@inject IJSRuntime JS + +@page "/Experiment" + +

Experiment Config

+ + + +
+
+ +
+ + @if (resultsAvailable) + { + + } + + @if (!running) + { + @if (resultsAvailable) + { +
+ + + @if (selected.HasValue && selected.Value) + { + + + + + +
Mesh@(selected.Value.Mesh.Name)
Material@(selected.Value.Mesh.Material.Name) (roughness: @(selected.Value.Mesh.Material.GetRoughness(selected.Value)), transmissive: @(selected.Value.Mesh.Material.IsTransmissive(selected.Value)))
Distance@(selected.Value.Distance)
Position@(selected.Value.Position)
+ } +
+ } + } + else + { +

Rendering...

+ } +
+ +@code { + SceneSelector sceneSelector; + Scene scene; + bool running = false; + bool resultsAvailable = false; + + SimpleImageIO.FlipBook flip; + IntegratorSelector integratorSelector; + + async Task OnSceneLoaded(SceneDirectory sceneDir) + { + await Task.Run(() => scene = sceneDir.SceneLoader.Scene); + flip = null; + resultsAvailable = false; + } + + async Task OnRunIntegrator(Integrator integrator) + { + running = true; + resultsAvailable = false; + + integratorSelector.Names.TryGetValue(integrator, out string customName); + await Task.Run(() => RunSingleIntegrator(integrator, customName)); + + running = false; + resultsAvailable = true; + } + + void OnDeleteIntegrator(Integrator integrator) + { + if (integrator == null || flip == null) return; + integratorSelector.Names.TryGetValue(integrator, out string customName); + string name = customName ?? IntegratorUtils.FormatClassName(integrator.GetType()); + flip.Remove(name); + } + + async Task OnRunAllIntegrators() + { + running = true; + resultsAvailable = false; + + flip = new FlipBook(660, 580) + .SetZoom(FlipBook.InitialZoom.FillWidth) + .SetToneMapper(FlipBook.InitialTMO.Exposure(scene.RecommendedExposure)) + .SetToolVisibility(false); + + foreach (var integrator in integratorSelector.addedIntegrators) + { + integratorSelector.Names.TryGetValue(integrator, out string customName); + await Task.Run(() => RunSingleIntegrator(integrator, customName)); + } + + running = false; + resultsAvailable = true; + } +} \ No newline at end of file diff --git a/ExperimentConfigTest/Pages/Experiment.razor.cs b/ExperimentConfigTest/Pages/Experiment.razor.cs new file mode 100644 index 00000000..d35676c2 --- /dev/null +++ b/ExperimentConfigTest/Pages/Experiment.razor.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Components; + +namespace ExperimentConfigTest.Pages; + +public partial class Experiment : ComponentBase +{ + const int Width = 1280; + const int Height = 720; + + SurfacePoint? selected; + + void OnFlipClick(FlipViewer.OnEventArgs args) + { + if (args.Control) + { + RNG rng = new(1241512); + var ray = scene.Camera.GenerateRay(new Vector2(args.MouseX + 0.5f, args.MouseY + 0.5f), ref rng).Ray; + selected = (SurfacePoint)scene.Raytracer.Trace(ray); + + SurfaceShader shader = new(selected.Value, -ray.Direction, false); + var s = shader.Sample(rng.NextFloat(), rng.NextFloat2D()); + Console.WriteLine(s); + } + } + + async Task OnDownloadClick() + { + HtmlReport report = new(); + report.AddMarkdown(""" + # Example experiment + $$ L_\mathrm{o} = \int_\Omega L_\mathrm{i} f_\mathrm{r} |\cos\theta_\mathrm{i}| \, d\omega_\mathrm{i} $$ + """); + report.AddFlipBook(flip); + await SeeSharp.Blazor.Scripts.DownloadAsFile(JS, "report.html", report.ToString()); + } + + void RunSingleIntegrator(Integrator integrator, string? name = null) + { + if (flip == null) { + flip = new FlipBook(660, 580) + .SetZoom(FlipBook.InitialZoom.FillWidth) + .SetToneMapper(FlipBook.InitialTMO.Exposure(scene.RecommendedExposure)) + .SetToolVisibility(false); + } + + scene.FrameBuffer = new(Width, Height, ""); + scene.Prepare(); + integrator.Render(scene); + + string displayName = name ?? IntegratorUtils.FormatClassName(integrator.GetType()); + flip.Remove(displayName); + flip.Add(displayName, scene.FrameBuffer.Image); + } +} \ No newline at end of file diff --git a/ExperimentConfigTest/Pages/Index.razor b/ExperimentConfigTest/Pages/Index.razor new file mode 100644 index 00000000..240dace6 --- /dev/null +++ b/ExperimentConfigTest/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("ExperimentConfigTest.Pages.", string.Empty); + if (name != "Index") + yield return (name, name); + } + } +} diff --git a/ExperimentConfigTest/Pages/_Host.cshtml b/ExperimentConfigTest/Pages/_Host.cshtml new file mode 100644 index 00000000..d76c8b4f --- /dev/null +++ b/ExperimentConfigTest/Pages/_Host.cshtml @@ -0,0 +1,34 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@namespace ExperimentConfigTest.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/ExperimentConfigTest/Program.cs b/ExperimentConfigTest/Program.cs new file mode 100644 index 00000000..ffae204c --- /dev/null +++ b/ExperimentConfigTest/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/ExperimentConfigTest/Properties/launchSettings.json b/ExperimentConfigTest/Properties/launchSettings.json new file mode 100644 index 00000000..4b0b1aac --- /dev/null +++ b/ExperimentConfigTest/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "iisSettings": { + "iisExpress": { + "applicationUrl": "http://localhost:18831", + "sslPort": 44326 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7055;http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ExperimentConfigTest/_Imports.razor b/ExperimentConfigTest/_Imports.razor new file mode 100644 index 00000000..c07841c4 --- /dev/null +++ b/ExperimentConfigTest/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using ExperimentConfigTest diff --git a/ExperimentConfigTest/appsettings.Development.json b/ExperimentConfigTest/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/ExperimentConfigTest/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ExperimentConfigTest/appsettings.json b/ExperimentConfigTest/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/ExperimentConfigTest/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ExperimentConfigTest/wwwroot/css/site.css b/ExperimentConfigTest/wwwroot/css/site.css new file mode 100644 index 00000000..ddc98cca --- /dev/null +++ b/ExperimentConfigTest/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 diff --git a/SeeSharp.Blazor/IntegratorSelector.razor b/SeeSharp.Blazor/IntegratorSelector.razor new file mode 100644 index 00000000..08bc3e09 --- /dev/null +++ b/SeeSharp.Blazor/IntegratorSelector.razor @@ -0,0 +1,135 @@ +@using System.Reflection +@using SeeSharp.Integrators +@using SeeSharp.Blazor +@using Microsoft.AspNetCore.Components.Forms + +@namespace SeeSharp.Blazor + +
+
+ + @foreach (var prop in IntegratorUtils.GetFilteredProps(typeof(Integrator))) + { + + } + +
+ +
+ + +
+ +
+
+ + + @if (undoStack.Count > 0) + { + + } +
+
+ + +
+
+
+ +
+ @foreach (var integrator in addedIntegrators) + { +
+ +
+ ⋮⋮ + + + + + + + +
+ + @if (IsExpanded(integrator)) + { +
+ @{ + var groups = GetParameterGroups(integrator); + bool isGlobalMuted = IsGlobalMuted(integrator); + var globalActive = isGlobalMuted ? globalIntegratorSettings : integrator; + } + + + @foreach (var group in groups) + { + bool isRoot = group.IsGlobal; + var target = (isRoot && isGlobalMuted) ? globalActive : integrator; + + RenderFragment renderContent = @
+ @foreach (var prop in group.Properties) + { + + } + @foreach (var field in group.Fields) + { + + } +
; + + RenderFragment actionButtons = @ + + + @if (settingsClipboard?.GroupTitle == group.Title) + { + + } + ; + + if (isRoot) + { +
+ + Global Settings @actionButtons + + + + +
@renderContent
+
+ } + else + { +
+ @group.Title @actionButtons +
@renderContent
+
+ } + } +
+
+ } +
+ } +
\ No newline at end of file diff --git a/SeeSharp.Blazor/IntegratorSelector.razor.cs b/SeeSharp.Blazor/IntegratorSelector.razor.cs new file mode 100644 index 00000000..19381319 --- /dev/null +++ b/SeeSharp.Blazor/IntegratorSelector.razor.cs @@ -0,0 +1,406 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.JSInterop; +using SeeSharp.Integrators; +using SeeSharp.Common; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace SeeSharp.Blazor; + +public partial class IntegratorSelector : ComponentBase +{ + [Inject] public IJSRuntime JS { get; set; } = default!; + [Parameter] public Scene scene { get; set; } = default!; + [Parameter] public EventCallback OnRunIntegrator { get; set; } + [Parameter] public EventCallback OnDeleteIntegrator { get; set; } + [Parameter] public EventCallback OnRunAllIntegrators { get; set; } + + public List addedIntegrators { get; private set; } = new(); + public Integrator globalIntegratorSettings { get; set; } = new PathTracer(); + public Dictionary Names { get; private set; } = new(); + + HashSet expandedItems = new(); + HashSet mutedIntegrators = new(); + Dictionary globalSettingsBackup = new(); + Integrator? draggedItem; + + Type[] integratorTypes = Array.Empty(); + string? selectedIntegrator; + private Stack<(Integrator Ptr, int Index, bool IsMuted, string? Name, Integrator? Backup, bool IsExpanded)> undoStack = new(); + private static (string GroupTitle, Dictionary Values)? settingsClipboard; + private const string LocalStorageKey = "SeeSharp_LastConfig"; + + 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.First().FullName; + } + DocumentationReader.LoadXmlDocumentation(typeof(Integrator).Assembly); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await LoadConfigFromBrowser(); + } + } + + // integrator management + void OnSelectionChanged(ChangeEventArgs e) => selectedIntegrator = e.Value?.ToString(); + + void AddIntegrator() + { + if (string.IsNullOrEmpty(selectedIntegrator)) return; + var type = integratorTypes.FirstOrDefault(t => t.FullName == selectedIntegrator); + if (type == null) return; + + var integrator = (Integrator)Activator.CreateInstance(type)!; + ApplyGlobalSettings(integrator); + Names[integrator] = FormatClassName(type); + mutedIntegrators.Add(integrator); + addedIntegrators.Add(integrator); + expandedItems.Add(integrator); + + _ = SaveConfigToBrowser(); + } + + void RunIntegrator(Integrator integrator) + { + _ = SaveConfigToBrowser(); + if (IsGlobalMuted(integrator)) + ApplyGlobalSettings(integrator); + OnRunIntegrator.InvokeAsync(integrator); + } + + void RunAllIntegrators() + { + _ = SaveConfigToBrowser(); + foreach (var integrator in addedIntegrators.Where(IsGlobalMuted)) + ApplyGlobalSettings(integrator); + OnRunAllIntegrators.InvokeAsync(); + } + + void DeleteIntegrator(Integrator integrator) + { + undoStack.Push((integrator, addedIntegrators.IndexOf(integrator), mutedIntegrators.Contains(integrator), + Names.GetValueOrDefault(integrator), globalSettingsBackup.GetValueOrDefault(integrator), expandedItems.Contains(integrator))); + + addedIntegrators.Remove(integrator); + mutedIntegrators.Remove(integrator); + globalSettingsBackup.Remove(integrator); + expandedItems.Remove(integrator); + Names.Remove(integrator); + + OnDeleteIntegrator.InvokeAsync(integrator); + _ = SaveConfigToBrowser(); + } + + void UndoDelete() + { + if (undoStack.TryPop(out var state)) + { + if (state.Index >= 0 && state.Index <= addedIntegrators.Count) + { + addedIntegrators.Insert(state.Index, state.Ptr); + } + else + { + addedIntegrators.Add(state.Ptr); + } + + if (state.IsMuted) mutedIntegrators.Add(state.Ptr); + if (state.IsExpanded) expandedItems.Add(state.Ptr); + + if (!string.IsNullOrEmpty(state.Name)) + Names[state.Ptr] = state.Name; + + if (state.Backup != null) + globalSettingsBackup[state.Ptr] = state.Backup; + } + } + + void DuplicateIntegrator(Integrator source) + { + var type = source.GetType(); + var newIntegrator = (Integrator)Activator.CreateInstance(type)!; + CopyAllProperties(source, newIntegrator); + + string sourceName = Names.GetValueOrDefault(source, FormatClassName(type)); + Names[newIntegrator] = GetNextAvailableName(sourceName); + + addedIntegrators.Add(newIntegrator); + if (mutedIntegrators.Contains(source)) + { + mutedIntegrators.Add(newIntegrator); + if (globalSettingsBackup.TryGetValue(source, out var backup)) + { + var backupClone = (Integrator)Activator.CreateInstance(type)!; + CopyAllProperties(backup, backupClone); + globalSettingsBackup[newIntegrator] = backupClone; + } + } + + expandedItems.Add(newIntegrator); + } + + // drag and panel state + void HandleDragStart(Integrator item) => draggedItem = item; + void HandleDrop(Integrator target) => draggedItem = null; + void HandleDragEnter(Integrator target) + { + if (draggedItem == null || target == null || draggedItem == target) return; + var oldIndex = addedIntegrators.IndexOf(draggedItem); + var newIndex = addedIntegrators.IndexOf(target); + if (oldIndex != newIndex) { + addedIntegrators.RemoveAt(oldIndex); + addedIntegrators.Insert(newIndex, draggedItem); + } + } + + bool IsExpanded(Integrator item) => expandedItems.Contains(item); + void ToggleExpand(Integrator item) + { + if (!expandedItems.Remove(item)) + expandedItems.Add(item); + } + + bool IsGlobalMuted(Integrator item) => mutedIntegrators.Contains(item); + void ToggleGlobalMute(Integrator item) + { + if (mutedIntegrators.Contains(item)) { + RestoreGlobalSettings(item); + mutedIntegrators.Remove(item); + } + else { + BackupGlobalSettings(item); + mutedIntegrators.Add(item); + } + } + + // copy and paste + void CopyGroupSettings(object targetObj, ParameterGroup group) + { + var values = new Dictionary(); + + foreach (var prop in group.Properties) + values[prop.Name] = prop.GetValue(targetObj); + foreach (var field in group.Fields) + values[field.Name] = field.GetValue(targetObj); + + settingsClipboard = (group.Title, values); + } + + void PasteGroupSettings(object targetObj, ParameterGroup group) + { + if (settingsClipboard.Value.GroupTitle != group.Title) + return; + + var values = settingsClipboard.Value.Values; + + foreach (var prop in group.Properties) + { + if (values.TryGetValue(prop.Name, out var val)) + prop.SetValue(targetObj, val); + } + foreach (var field in group.Fields) + { + if (values.TryGetValue(field.Name, out var val)) + field.SetValue(targetObj, val); + } + } + + void CopyAllProperties(Integrator source, Integrator target) + { + foreach (var group in IntegratorUtils.GetParameterGroups(source)) + { + foreach (var prop in group.Properties.Where(p => p.CanRead && p.CanWrite)) + prop.SetValue(target, prop.GetValue(source)); + foreach (var field in group.Fields) + field.SetValue(target, field.GetValue(source)); + } + } + + void ApplyGlobalSettings(Integrator integrator) => CopyGlobalProperties(globalIntegratorSettings, integrator); + + void BackupGlobalSettings(Integrator integrator) + { + var backup = (Integrator)Activator.CreateInstance(integrator.GetType())!; + CopyGlobalProperties(integrator, backup); + globalSettingsBackup[integrator] = backup; + } + + void RestoreGlobalSettings(Integrator integrator) + { + if (globalSettingsBackup.TryGetValue(integrator, out var backup)) + { + CopyGlobalProperties(backup, integrator); + globalSettingsBackup.Remove(integrator); + } + } + + void CopyGlobalProperties(Integrator source, Integrator target) + { + var props = IntegratorUtils.GetFilteredProps(typeof(Integrator)); + + foreach (var prop in props) { + if (prop.CanRead && prop.CanWrite) + { + prop.SetValue(target, prop.GetValue(source)); + } + } + } + + // import and output config + async Task SaveConfigToBrowser() => await JS.InvokeVoidAsync("localStorage.setItem", LocalStorageKey, JsonSerializer.Serialize(CreateConfigSnapshot())); + + async Task LoadConfigFromBrowser() + { + var json = await JS.InvokeAsync("localStorage.getItem", LocalStorageKey); + if (!string.IsNullOrEmpty(json)) + { + LoadConfigFromSnapshot(JsonSerializer.Deserialize(json)); + } + } + + async Task DownloadConfig() + { + var dto = CreateConfigSnapshot(); + var json = JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true }); + var fileBytes = System.Text.Encoding.UTF8.GetBytes(json); + using var stream = new MemoryStream(fileBytes); + using var streamRef = new DotNetStreamReference(stream); + await JS.InvokeVoidAsync("downloadFileFromStream", "seesharp-config.json", streamRef); + } + + async Task UploadConfig(InputFileChangeEventArgs e) + { + if (e.File == null) return; + using var reader = new StreamReader(e.File.OpenReadStream(1024 * 1024)); + var dto = JsonSerializer.Deserialize(await reader.ReadToEndAsync()); + if (dto != null) + { + LoadConfigFromSnapshot(dto); await SaveConfigToBrowser(); + } + } + + IntegratorConfigDTO CreateConfigSnapshot() + { + var dto = new IntegratorConfigDTO(); + + foreach (var p in IntegratorUtils.GetFilteredProps(typeof(Integrator)).Where(p => p.CanRead)) + dto.GlobalSettings[p.Name] = p.GetValue(globalIntegratorSettings)!; + + dto.Integrators = addedIntegrators.Select(i => { + var state = new IntegratorStateDTO { + AssemblyQualifiedName = i.GetType().AssemblyQualifiedName, + CustomName = Names.GetValueOrDefault(i, ""), + IsMuted = mutedIntegrators.Contains(i) + }; + foreach (var group in IntegratorUtils.GetParameterGroups(i)) { + foreach (var p in group.Properties.Where(p => p.CanRead)) + state.Parameters[p.Name] = p.GetValue(i)!; + foreach (var f in group.Fields) + state.Parameters[f.Name] = f.GetValue(i)!; + } + return state; + }).ToList(); + + return dto; + } + + void LoadConfigFromSnapshot(IntegratorConfigDTO dto) + { + addedIntegrators.Clear(); + mutedIntegrators.Clear(); + Names.Clear(); + expandedItems.Clear(); + globalSettingsBackup.Clear(); + undoStack.Clear(); + + foreach (var kvp in dto.GlobalSettings) + { + var prop = typeof(Integrator).GetProperty(kvp.Key); + if (prop != null && prop.CanWrite) + { + prop.SetValue(globalIntegratorSettings, ConvertJsonElement(kvp.Value, prop.PropertyType)); + } + } + + foreach (var state in dto.Integrators) + { + var type = Type.GetType(state.AssemblyQualifiedName); + if (type == null) continue; + var integrator = (Integrator)Activator.CreateInstance(type)!; + + foreach (var kvp in state.Parameters) + { + var prop = type.GetProperty(kvp.Key); + if (prop != null && prop.CanWrite) + { + prop.SetValue(integrator, ConvertJsonElement(kvp.Value, prop.PropertyType)); + continue; + } + var field = type.GetField(kvp.Key); + if (field != null) + { + field.SetValue(integrator, ConvertJsonElement(kvp.Value, field.FieldType)); + } + } + + addedIntegrators.Add(integrator); + if (!string.IsNullOrEmpty(state.CustomName)) + Names[integrator] = state.CustomName; + if (state.IsMuted) + { + mutedIntegrators.Add(integrator); + BackupGlobalSettings(integrator); + } + expandedItems.Add(integrator); + } + StateHasChanged(); + } + + object? ConvertJsonElement(object? jsonValue, Type targetType) => jsonValue is JsonElement el ? JsonSerializer.Deserialize(el.GetRawText(), targetType) : jsonValue; + + // helpers + string GetNextAvailableName(string originalName) + { + string baseName = Regex.Match(originalName, @"^(.*?) - Copy(?: (\d+))?$").Success ? Regex.Match(originalName, @"^(.*?) - Copy(?: (\d+))?$").Groups[1].Value : originalName; + int counter = 1; + while (Names.Values.Any(n => n == (counter == 1 ? $"{baseName} - Copy" : $"{baseName} - Copy {counter}"))) + counter++; + return counter == 1 ? $"{baseName} - Copy" : $"{baseName} - Copy {counter}"; + } + + protected List GetParameterGroups(Integrator integrator) + => IntegratorUtils.GetParameterGroups(integrator); + + protected string FormatClassName(Type t) => IntegratorUtils.FormatClassName(t); + + public class IntegratorConfigDTO + { + public Dictionary GlobalSettings { get; set; } = new(); + public List Integrators { get; set; } = new(); + } + public class IntegratorStateDTO + { + public string? AssemblyQualifiedName { get; set; } + public string? CustomName { get; set; } + public bool IsMuted { get; set; } + public Dictionary Parameters { get; set; } = new(); + } +} diff --git a/SeeSharp.Blazor/IntegratorSelector.razor.css b/SeeSharp.Blazor/IntegratorSelector.razor.css new file mode 100644 index 00000000..a4a4eca9 --- /dev/null +++ b/SeeSharp.Blazor/IntegratorSelector.razor.css @@ -0,0 +1,180 @@ +.integrator-selector { + display: flex; + flex-direction: column; + gap: 0.4em; +} + +.integrator-container { + display: flex; + flex-direction: column; + gap: 0.1em; +} + +.integrator-box { + float: left; + margin-right: 1em; + width: 250px; +} + +.integrator-header { + display: flex; + align-items: center; + padding: 4px 0; + cursor: pointer; + user-select: none; + background: transparent; + border: none; +} + +.drag-handle { + cursor: grab; + font-size: 1.2em; + color: #bbb; + padding: 0 8px 0 4px; +} +.drag-handle:active { + cursor: grabbing; + color: #666; +} + +.arrow { + margin-right: 6px; + font-size: 0.75em; + width: 12px; + text-align: center; + transition: transform 0.2s; +} +.arrow.down { transform: rotate(90deg); } + +.title-input { + background: transparent; + border: 1px solid transparent; + flex-grow: 1; + width: 100px; + font-weight: 600; + padding: 2px 4px; + margin-left: 0; + border-radius: 4px; + transition: all 0.2s; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.title-input:hover { background-color: rgba(0,0,0,0.03); } +.title-input:focus { + background-color: white; + border-color: #2196F3; + outline: none; + text-overflow: clip; +} + +details { + margin-top: 2px; + width: 100%; +} +summary { + cursor: pointer; + font-weight: normal; + font-size: smaller; + color: black; + outline: none; + display: flex; + align-items: center; +} +summary:hover { color: #000; } +summary::before { + content: '▶'; + font-size: 0.7em; + margin-right: 6px; + display: inline-block; + transition: transform 0.2s; + color: black; +} +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; +} + +.integrator-content { + padding: 0; + border: none; + background: transparent; +} +.integrator-content ::deep .render-setting-title { + display: none !important; + +} +.integrator-content ::deep input, +.integrator-content ::deep select { + max-width: 100%; + flex-shrink: 1; + min-width: 0; +} + +.mute-label { + font-size: 0.8em; + color: #888; + margin-left: auto; + display: flex; + align-items: center; +} + +.muted-container { + opacity: 0.5; + pointer-events: none; + filter: grayscale(1); + transition: opacity 0.2s ease; +} + +.header-icon-btn { + display: inline-flex; + justify-content: center; + align-items: center; + width: 20px; + height: 20px; + background: transparent; + border: none; + cursor: pointer; + font-size: 1em; + color: #666; + transition: color 0.2s, transform 0.1s; + line-height: 1; +} + +.group-actions { + margin-left: auto; + display: inline-flex; + gap: 4px; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; +} + +summary:hover .group-actions, +.leaf-header:hover .group-actions { + opacity: 1; + pointer-events: auto; +} + +.icon-btn-small { + background: transparent; + border: none; + cursor: pointer; + font-size: 1.1em; + padding: 2px 4px; + border-radius: 4px; + color: #666; +} +.icon-btn-small:hover { + background-color: rgba(0, 0, 0, 0.05); + color: #000; +} \ No newline at end of file diff --git a/SeeSharp.Blazor/IntegratorUtils.cs b/SeeSharp.Blazor/IntegratorUtils.cs new file mode 100644 index 00000000..40b6f794 --- /dev/null +++ b/SeeSharp.Blazor/IntegratorUtils.cs @@ -0,0 +1,96 @@ +using System.Reflection; +using SeeSharp.Integrators; +using SeeSharp.Common; + +namespace SeeSharp.Blazor; + +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 bool IsGlobal { get; set; } +} + +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(), + IsGlobal = isGlobalSettings, + }; + + 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.Blazor/RenderSetting.razor b/SeeSharp.Blazor/RenderSetting.razor new file mode 100644 index 00000000..a1efea55 --- /dev/null +++ b/SeeSharp.Blazor/RenderSetting.razor @@ -0,0 +1,41 @@ +@using System.Reflection + +@namespace SeeSharp.Blazor + +@{ + 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 From 57e4274a6eab63693579cab00c347afe093518bc Mon Sep 17 00:00:00 2001 From: mzwang34 <“mzwang3434@gmail.com”> Date: Sun, 22 Feb 2026 11:54:48 +0100 Subject: [PATCH 2/4] adjust UI --- SeeSharp.Blazor/IntegratorSelector.razor | 8 +++----- SeeSharp.Blazor/IntegratorSelector.razor.css | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/SeeSharp.Blazor/IntegratorSelector.razor b/SeeSharp.Blazor/IntegratorSelector.razor index 08bc3e09..e0541aef 100644 --- a/SeeSharp.Blazor/IntegratorSelector.razor +++ b/SeeSharp.Blazor/IntegratorSelector.razor @@ -28,7 +28,7 @@ -
+
@@ -58,12 +58,10 @@
⋮⋮ - + - -
@if (IsExpanded(integrator)) @@ -99,7 +97,7 @@
; RenderFragment actionButtons = @ - + @if (settingsClipboard?.GroupTitle == group.Title) { diff --git a/SeeSharp.Blazor/IntegratorSelector.razor.css b/SeeSharp.Blazor/IntegratorSelector.razor.css index a4a4eca9..d249bf56 100644 --- a/SeeSharp.Blazor/IntegratorSelector.razor.css +++ b/SeeSharp.Blazor/IntegratorSelector.razor.css @@ -177,4 +177,14 @@ summary:hover .group-actions, .icon-btn-small:hover { background-color: rgba(0, 0, 0, 0.05); color: #000; +} + +.main-actions button { + flex: 0 0 80px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + white-space: nowrap; } \ No newline at end of file From 724214d98edd7a12cea4ffc0cf26c2a1c8e32d10 Mon Sep 17 00:00:00 2001 From: mzwang34 <“mzwang3434@gmail.com”> Date: Thu, 26 Feb 2026 16:58:30 +0100 Subject: [PATCH 3/4] improve default integrator name --- SeeSharp.Blazor/IntegratorSelector.razor.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SeeSharp.Blazor/IntegratorSelector.razor.cs b/SeeSharp.Blazor/IntegratorSelector.razor.cs index 19381319..1e8f08c7 100644 --- a/SeeSharp.Blazor/IntegratorSelector.razor.cs +++ b/SeeSharp.Blazor/IntegratorSelector.razor.cs @@ -70,7 +70,17 @@ void AddIntegrator() var integrator = (Integrator)Activator.CreateInstance(type)!; ApplyGlobalSettings(integrator); - Names[integrator] = FormatClassName(type); + + string baseName = FormatClassName(type); + string finalName = baseName; + int cnt = 2; + while (Names.Values.Contains(finalName)) + { + finalName = $"{baseName} {cnt}"; + cnt++; + } + Names[integrator] = finalName; + mutedIntegrators.Add(integrator); addedIntegrators.Add(integrator); expandedItems.Add(integrator); From 01a420b967e66a2a5bcd3dc79e94c91e81fc31aa Mon Sep 17 00:00:00 2001 From: mzwang34 <“mzwang3434@gmail.com”> Date: Thu, 26 Feb 2026 17:23:33 +0100 Subject: [PATCH 4/4] add struct setting --- SeeSharp.Blazor/IntegratorSelector.razor.css | 16 +++---- SeeSharp.Blazor/IntegratorUtils.cs | 6 +-- SeeSharp.Blazor/RenderSetting.razor | 8 ++++ SeeSharp.Blazor/StructSetting.razor | 47 ++++++++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 SeeSharp.Blazor/StructSetting.razor diff --git a/SeeSharp.Blazor/IntegratorSelector.razor.css b/SeeSharp.Blazor/IntegratorSelector.razor.css index d249bf56..0f3b8134 100644 --- a/SeeSharp.Blazor/IntegratorSelector.razor.css +++ b/SeeSharp.Blazor/IntegratorSelector.razor.css @@ -68,11 +68,11 @@ text-overflow: clip; } -details { +::deep details { margin-top: 2px; width: 100%; } -summary { +::deep summary { cursor: pointer; font-weight: normal; font-size: smaller; @@ -81,8 +81,8 @@ summary { display: flex; align-items: center; } -summary:hover { color: #000; } -summary::before { +::deep summary:hover { color: #000; } +::deep summary::before { content: '▶'; font-size: 0.7em; margin-right: 6px; @@ -90,17 +90,17 @@ summary::before { transition: transform 0.2s; color: black; } -details[open] > summary::before { transform: rotate(90deg); } -summary::-webkit-details-marker { display: none; } +::deep details[open] > summary::before { transform: rotate(90deg); } +::deep summary::-webkit-details-marker { display: none; } -details > div { +::deep details > div { margin-left: 6px; padding-left: 12px; border-left: 2px solid #e0e0e0; margin-top: 2px; margin-bottom: 2px; } -details:hover > div { +::deep details:hover > div { border-left-color: #ccc; } diff --git a/SeeSharp.Blazor/IntegratorUtils.cs b/SeeSharp.Blazor/IntegratorUtils.cs index 40b6f794..a62b8e29 100644 --- a/SeeSharp.Blazor/IntegratorUtils.cs +++ b/SeeSharp.Blazor/IntegratorUtils.cs @@ -80,13 +80,13 @@ public static bool IsVisible(MemberInfo m) Type t = (m is PropertyInfo pi) ? pi.PropertyType : ((FieldInfo)m).FieldType; Type underlyingType = Nullable.GetUnderlyingType(t) ?? t; - return underlyingType.IsPrimitive; + return underlyingType.IsValueType; } - public static IEnumerable GetFilteredProps(Type type) => + public static IEnumerable GetFilteredProps(Type type) => type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); - public static IEnumerable GetFilteredFields(Type type) => + public static IEnumerable GetFilteredFields(Type type) => type.GetFields(BindingFlags.Public | BindingFlags.Instance).Where(IsVisible); public static string GetDescription(MemberInfo member) diff --git a/SeeSharp.Blazor/RenderSetting.razor b/SeeSharp.Blazor/RenderSetting.razor index a1efea55..6a39a65a 100644 --- a/SeeSharp.Blazor/RenderSetting.razor +++ b/SeeSharp.Blazor/RenderSetting.razor @@ -32,6 +32,14 @@ Setter(Convert.ChangeType(v, underlyingType)); })" /> } + else if (underlyingType.IsValueType && !underlyingType.IsPrimitive && !underlyingType.IsEnum) + { + + } } @code { diff --git a/SeeSharp.Blazor/StructSetting.razor b/SeeSharp.Blazor/StructSetting.razor new file mode 100644 index 00000000..c94f58df --- /dev/null +++ b/SeeSharp.Blazor/StructSetting.razor @@ -0,0 +1,47 @@ +@using System.Reflection + +@namespace SeeSharp.Blazor + +
+ @Name +
+ @foreach (var prop in TargetType.GetProperties()) + { + + } + + @foreach (var field in TargetType.GetFields()) + { + + } +
+
+ +@code { + [Parameter] public string Name { get; set; } = string.Empty; + [Parameter] public Type TargetType { get; set; } = default!; + [Parameter] public Func Getter { get; set; } = default!; + [Parameter] public Action Setter { get; set; } = default!; + + private object StructInstance => Getter() ?? Activator.CreateInstance(TargetType)!; + + private void UpdateProperty(PropertyInfo prop, object val) + { + var obj = StructInstance; + prop.SetValue(obj, val); + Setter(obj); + } + + private void UpdateField(FieldInfo field, object val) + { + var obj = StructInstance; + field.SetValue(obj, val); + Setter(obj); + } +} \ No newline at end of file