sandpypi/Sand.App/SandApp.Rendering.cs

253 lines
14 KiB
C#

using Raylib_cs;
using Sand.Core;
using System.Numerics;
using System.Diagnostics;
namespace Sand.App;
internal static partial class SandApp
{
private static void Draw(AppState state)
{
var buildStart = Stopwatch.GetTimestamp();
var rgbaFrame = state.Simulation.BuildRgbaFrame();
state.LastFrameBuildCallTimeMicroseconds = ToMicroseconds(buildStart, Stopwatch.GetTimestamp());
var uploadStart = Stopwatch.GetTimestamp();
unsafe
{
fixed (byte* pixels = rgbaFrame)
{
Raylib.UpdateTexture(state.FrameTexture, pixels);
}
}
state.LastTextureUploadTimeMicroseconds = ToMicroseconds(uploadStart, Stopwatch.GetTimestamp());
var mouse = Raylib.GetMousePosition();
var worldRect = new Rectangle(SidebarWidth, 0, state.SimWidth * ParticleSize, state.SimHeight * ParticleSize);
var settingsRect = GetSettingsRect(state);
var drawStart = Stopwatch.GetTimestamp();
Raylib.BeginDrawing();
Raylib.ClearBackground(new Color(18, 18, 24, 255));
DrawSidebar(state);
Raylib.DrawTextureEx(state.FrameTexture, new Vector2(SidebarWidth, 0), 0f, ParticleSize, Color.White);
if (state.Settings.EnableCursor)
{
DrawBrushCursor(mouse, state.BrushRadius);
}
if (state.ZoomEnabled && Raylib.CheckCollisionPointRec(mouse, worldRect))
{
DrawZoom(state.FrameTexture, mouse, state.SimWidth, state.SimHeight);
}
if (state.SettingsVisible)
{
DrawSettingsPanel(state, settingsRect, state.SettingItems);
}
DrawStatusBar(state);
if (state.Settings.EnableDebug || state.Settings.EnableFps)
{
DrawDebug(state, Raylib.GetFrameTime());
}
Raylib.EndDrawing();
state.LastDrawTimeMicroseconds = ToMicroseconds(drawStart, Stopwatch.GetTimestamp());
}
private static void DrawSidebar(AppState state)
{
Raylib.DrawRectangle(0, 0, SidebarWidth, Raylib.GetScreenHeight(), new Color(28, 32, 40, 255));
Raylib.DrawText($"Sand C# ({state.BackendName})", 12, 16, 24, new Color(100, 170, 255, 255));
var y = 60;
var categoryTop = 60;
var categoryHeight = 180;
var visibleCategoryRows = GetVisibleCategoryRows();
Raylib.BeginScissorMode(12, categoryTop, 196, categoryHeight);
foreach (var category in state.Categories.Keys.Skip(state.CategoryScrollOffset).Take(visibleCategoryRows))
{
var selected = category == state.CurrentCategory;
Raylib.DrawRectangleRounded(new Rectangle(12, y, 196, 32), 0.2f, 6, selected ? new Color(66, 128, 182, 255) : new Color(48, 54, 66, 255));
Raylib.DrawText(category, 22, y + 8, 18, Color.White);
y += 38;
}
Raylib.EndScissorMode();
var categoryCount = state.Categories.Count;
if (categoryCount > visibleCategoryRows)
{
var trackRect = new Rectangle(202, categoryTop, 6, categoryHeight);
var thumbHeight = MathF.Max(24f, categoryHeight * (visibleCategoryRows / (float)categoryCount));
var maxOffset = categoryCount - visibleCategoryRows;
var thumbTravel = MathF.Max(0f, categoryHeight - thumbHeight);
var thumbY = categoryTop + ((state.CategoryScrollOffset / (float)maxOffset) * thumbTravel);
Raylib.DrawRectangleRounded(trackRect, 0.4f, 4, new Color(52, 58, 72, 255));
Raylib.DrawRectangleRounded(new Rectangle(trackRect.X, thumbY, trackRect.Width, thumbHeight), 0.4f, 4, new Color(124, 140, 164, 255));
}
y = 240;
var listTop = 240;
var listHeight = Math.Max(28, Raylib.GetScreenHeight() - 404);
var visibleRows = GetVisibleParticleRows();
Raylib.BeginScissorMode(12, listTop, 196, listHeight);
foreach (var particle in state.Categories[state.CurrentCategory].Skip(state.ParticleScrollOffset).Take(visibleRows))
{
var selected = particle.Id == state.CurrentParticle;
var bg = selected ? new Color(230, 230, 240, 255) : new Color(particle.Color.R, particle.Color.G, particle.Color.B, (byte)255);
Raylib.DrawRectangle(12, y, 196, 24, bg);
Raylib.DrawText(particle.Name, 18, y + 4, 16, Color.Black);
y += 28;
}
Raylib.EndScissorMode();
var particleCount = state.Categories[state.CurrentCategory].Count;
if (particleCount > visibleRows)
{
var trackRect = new Rectangle(202, listTop, 6, listHeight);
var thumbHeight = MathF.Max(24f, listHeight * (visibleRows / (float)particleCount));
var maxOffset = particleCount - visibleRows;
var thumbTravel = MathF.Max(0f, listHeight - thumbHeight);
var thumbY = listTop + ((state.ParticleScrollOffset / (float)maxOffset) * thumbTravel);
Raylib.DrawRectangleRounded(trackRect, 0.4f, 4, new Color(52, 58, 72, 255));
Raylib.DrawRectangleRounded(new Rectangle(trackRect.X, thumbY, trackRect.Width, thumbHeight), 0.4f, 4, new Color(124, 140, 164, 255));
}
Raylib.DrawText($"Brush {state.BrushRadius}", 12, 204, 18, Color.White);
Raylib.DrawText(state.Settings.PauseSim ? "Paused" : "Running", 120, 204, 18, state.Settings.PauseSim ? Color.Yellow : Color.Green);
Raylib.DrawText($"Particles {state.Simulation.ParticleCount}", 12, Raylib.GetScreenHeight() - 126, 18, Color.LightGray);
Raylib.DrawRectangleRounded(new Rectangle(12, Raylib.GetScreenHeight() - 96, 196, 32), 0.2f, 6, new Color(160, 58, 58, 255));
Raylib.DrawText("Clear Grid", 62, Raylib.GetScreenHeight() - 88, 18, Color.White);
Raylib.DrawRectangleRounded(new Rectangle(12, Raylib.GetScreenHeight() - 54, 196, 32), 0.2f, 6, state.SettingsVisible ? new Color(98, 112, 136, 255) : new Color(72, 80, 90, 255));
Raylib.DrawText("Settings", 68, Raylib.GetScreenHeight() - 46, 18, Color.White);
}
private static void DrawSettingsPanel(AppState state, Rectangle panelRect, IReadOnlyList<SettingItem> items)
{
Raylib.DrawRectangleRounded(panelRect, 0.08f, 8, new Color(18, 22, 28, 240));
Raylib.DrawRectangleLinesEx(panelRect, 1.5f, new Color(110, 140, 170, 255));
Raylib.DrawText("Engine Settings", (int)panelRect.X + 14, (int)panelRect.Y + 10, 22, Color.White);
var y = panelRect.Y + 18;
var listTop = panelRect.Y + 18;
var listHeight = Math.Max(28f, panelRect.Height - 68f);
var visibleRows = GetVisibleSettingsRows(panelRect);
Raylib.BeginScissorMode((int)panelRect.X + 12, (int)listTop, (int)panelRect.Width - 24, (int)listHeight);
foreach (var item in items.Skip(state.SettingsScrollOffset).Take(visibleRows))
{
var rowRect = new Rectangle(panelRect.X + 12, y, panelRect.Width - 24, 26);
Raylib.DrawRectangleRounded(rowRect, 0.15f, 6, item.Get() ? new Color(63, 117, 87, 255) : new Color(63, 69, 78, 255));
Raylib.DrawText(item.Label, (int)rowRect.X + 10, (int)rowRect.Y + 5, 18, Color.White);
Raylib.DrawText(item.Get() ? "On" : "Off", (int)rowRect.X + (int)rowRect.Width - 38, (int)rowRect.Y + 5, 18, Color.White);
y += 34;
}
Raylib.EndScissorMode();
if (items.Count > visibleRows)
{
var trackRect = new Rectangle(panelRect.X + panelRect.Width - 10, listTop, 4, listHeight);
var thumbHeight = MathF.Max(24f, listHeight * (visibleRows / (float)items.Count));
var maxOffset = items.Count - visibleRows;
var thumbTravel = MathF.Max(0f, listHeight - thumbHeight);
var thumbY = listTop + ((state.SettingsScrollOffset / (float)maxOffset) * thumbTravel);
Raylib.DrawRectangleRounded(trackRect, 0.4f, 4, new Color(52, 58, 72, 255));
Raylib.DrawRectangleRounded(new Rectangle(trackRect.X, thumbY, trackRect.Width, thumbHeight), 0.4f, 4, new Color(124, 140, 164, 255));
}
var speedDownRect = new Rectangle(panelRect.X + 12, panelRect.Y + panelRect.Height - 36, 48, 24);
var speedUpRect = new Rectangle(panelRect.X + 72, panelRect.Y + panelRect.Height - 36, 48, 24);
Raylib.DrawRectangleRounded(speedDownRect, 0.15f, 6, new Color(63, 69, 78, 255));
Raylib.DrawRectangleRounded(speedUpRect, 0.15f, 6, new Color(63, 69, 78, 255));
Raylib.DrawText("-", (int)speedDownRect.X + 18, (int)speedDownRect.Y + 4, 20, Color.White);
Raylib.DrawText("+", (int)speedUpRect.X + 17, (int)speedUpRect.Y + 3, 20, Color.White);
}
private static void DrawStatusBar(AppState state)
{
var y = Raylib.GetScreenHeight() - 28;
Raylib.DrawRectangle(SidebarWidth, y, Raylib.GetScreenWidth() - SidebarWidth, 28, new Color(12, 14, 18, 220));
var stats = state.Simulation.FrameStats;
var status = $"Current {state.CurrentParticle} | Brush {state.BrushRadius} | Speed {state.Settings.TimeScale:0.0}x @ {state.Settings.SimulationStepsPerSecond:0}Hz | Cells {stats.ProcessedCells} | Bounds {stats.MinActiveX},{stats.MinActiveY} - {stats.MaxActiveX},{stats.MaxActiveY}";
if (stats.LoadedChunkCount > 0 || stats.ActiveChunkCount > 0)
{
status += $" | Chunks {stats.ActiveChunkCount}/{stats.LoadedChunkCount}";
status += $" step {stats.SteppedChunkCount} sleep {stats.SleepingChunkCount}";
}
if (state.Settings.WrapParticles)
{
status += " | Wrap";
}
if (state.Settings.OuterWall)
{
status += " | OuterWall";
}
Raylib.DrawText(status, SidebarWidth + 10, y + 6, 16, Color.LightGray);
}
private static void DrawBrushCursor(Vector2 mouse, int brushRadius)
{
if (mouse.X < SidebarWidth)
{
return;
}
Raylib.DrawCircleLines((int)mouse.X, (int)mouse.Y, brushRadius * ParticleSize, new Color(255, 255, 255, 180));
}
private static void DrawZoom(Texture2D frameTexture, Vector2 mouse, int simWidth, int simHeight)
{
var simMouseX = Math.Clamp((int)((mouse.X - SidebarWidth) / ParticleSize), 0, simWidth - 1);
var simMouseY = Math.Clamp((int)(mouse.Y / ParticleSize), 0, simHeight - 1);
var srcX = Math.Clamp(simMouseX - 8, 0, simWidth - 16);
var srcY = Math.Clamp(simMouseY - 8, 0, simHeight - 16);
var source = new Rectangle(srcX, srcY, 16, 16);
var target = new Rectangle(Raylib.GetScreenWidth() - 220, 20, 180, 180);
Raylib.DrawRectangleRounded(target, 0.1f, 6, new Color(22, 22, 30, 230));
Raylib.DrawTexturePro(frameTexture, source, target, Vector2.Zero, 0f, Color.White);
Raylib.DrawRectangleLinesEx(target, 2f, new Color(255, 255, 255, 160));
}
private static void DrawDebug(AppState state, float dt)
{
var rect = new Rectangle(236, 12, 430, 256);
Raylib.DrawRectangleRounded(rect, 0.15f, 6, new Color(16, 16, 16, 190));
var y = 24;
if (state.Settings.EnableFps)
{
Raylib.DrawText($"FPS {Raylib.GetFPS()}", 248, y, 18, Color.White);
y += 20;
}
if (state.Settings.EnableDebug)
{
var stats = state.Simulation.FrameStats;
Raylib.DrawText($"Frame {state.Simulation.Frame}", 248, y, 18, Color.White);
Raylib.DrawText($"Particle {state.CurrentParticle}", 248, y + 20, 18, Color.White);
Raylib.DrawText($"Brush {state.BrushRadius} dt {dt:0.000}", 248, y + 40, 18, Color.White);
Raylib.DrawText($"Processed {stats.ProcessedCells} particles {stats.ParticleCount}", 248, y + 60, 18, Color.White);
Raylib.DrawText($"Bounds {stats.MinActiveX},{stats.MinActiveY} - {stats.MaxActiveX},{stats.MaxActiveY}", 248, y + 80, 18, Color.White);
if (stats.LoadedChunkCount > 0 || stats.ActiveChunkCount > 0)
{
Raylib.DrawText($"Chunks loaded {stats.LoadedChunkCount} active {stats.ActiveChunkCount} dirty {stats.DirtyChunkCount}", 248, y + 100, 18, Color.White);
Raylib.DrawText($"Moves {stats.MovedParticleCount} swaps {stats.SwappedParticleCount} attempts {stats.MoveAttemptCount} stalled {stats.StalledMovableCount}", 248, y + 120, 18, Color.White);
Raylib.DrawText($"Att v {stats.VerticalMoveAttemptCount} d {stats.DiagonalMoveAttemptCount} l {stats.LateralMoveAttemptCount} swap {stats.SwapAttemptCount}", 248, y + 140, 18, Color.White);
Raylib.DrawText($"Fast {stats.MovementOnlyFastPathCount} full {stats.FullRuntimeStepCount} s {stats.FullRuntimeSolidCount} l {stats.FullRuntimeLiquidCount} g {stats.FullRuntimeGasCount}", 248, y + 160, 18, Color.White);
Raylib.DrawText($"ms act {stats.ActivationTimeMicroseconds / 1000f:0.00} move {stats.MovementTimeMicroseconds / 1000f:0.00} run {stats.RuntimeTimeMicroseconds / 1000f:0.00} render {stats.RenderTimeMicroseconds / 1000f:0.00}", 248, y + 180, 18, Color.White);
Raylib.DrawText($"app frame {state.LastAppFrameTimeMicroseconds / 1000f:0.00} upd {state.LastUpdateTimeMicroseconds / 1000f:0.00} sim {state.LastSimulationLoopTimeMicroseconds / 1000f:0.00} steps {state.LastSimulationStepCount}", 248, y + 200, 18, Color.White);
Raylib.DrawText($"app build {state.LastFrameBuildCallTimeMicroseconds / 1000f:0.00} upload {state.LastTextureUploadTimeMicroseconds / 1000f:0.00} draw {state.LastDrawTimeMicroseconds / 1000f:0.00} other {state.LastAppOtherTimeMicroseconds / 1000f:0.00}", 248, y + 220, 18, Color.White);
}
else
{
Raylib.DrawText($"app frame {state.LastAppFrameTimeMicroseconds / 1000f:0.00} upd {state.LastUpdateTimeMicroseconds / 1000f:0.00} sim {state.LastSimulationLoopTimeMicroseconds / 1000f:0.00} steps {state.LastSimulationStepCount}", 248, y + 100, 18, Color.White);
Raylib.DrawText($"app build {state.LastFrameBuildCallTimeMicroseconds / 1000f:0.00} upload {state.LastTextureUploadTimeMicroseconds / 1000f:0.00} draw {state.LastDrawTimeMicroseconds / 1000f:0.00} other {state.LastAppOtherTimeMicroseconds / 1000f:0.00}", 248, y + 120, 18, Color.White);
}
}
}
}