253 lines
14 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|