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 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); } } } }