333 lines
12 KiB
C#
333 lines
12 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 Update(AppState state)
|
|
{
|
|
var updateStart = Stopwatch.GetTimestamp();
|
|
var dt = Raylib.GetFrameTime();
|
|
var mouse = Raylib.GetMousePosition();
|
|
var worldRect = new Rectangle(SidebarWidth, 0, state.SimWidth * ParticleSize, state.SimHeight * ParticleSize);
|
|
var overSidebar = mouse.X < SidebarWidth;
|
|
var settingsRect = GetSettingsRect(state);
|
|
|
|
HandleHotkeys(state);
|
|
HandleMouseWheel(state, mouse, overSidebar);
|
|
HandleWorldPainting(state, mouse, worldRect, overSidebar, settingsRect);
|
|
HandleSidebarClick(state, mouse);
|
|
StepSimulation(state, dt, mouse);
|
|
|
|
state.PreviousMouse = mouse;
|
|
state.LastUpdateTimeMicroseconds = ToMicroseconds(updateStart, Stopwatch.GetTimestamp());
|
|
}
|
|
|
|
private static void HandleHotkeys(AppState state)
|
|
{
|
|
if (Raylib.IsKeyPressed(KeyboardKey.Space))
|
|
{
|
|
state.Settings.PauseSim = !state.Settings.PauseSim;
|
|
}
|
|
|
|
if (Raylib.IsKeyPressed(KeyboardKey.C))
|
|
{
|
|
state.Simulation.Clear();
|
|
}
|
|
|
|
if (Raylib.IsKeyPressed(KeyboardKey.Z))
|
|
{
|
|
state.ZoomEnabled = !state.ZoomEnabled;
|
|
}
|
|
|
|
if (Raylib.IsKeyPressed(KeyboardKey.F3))
|
|
{
|
|
state.Settings.EnableDebug = !state.Settings.EnableDebug;
|
|
}
|
|
|
|
if (Raylib.IsKeyPressed(KeyboardKey.Tab))
|
|
{
|
|
state.SettingsVisible = !state.SettingsVisible;
|
|
}
|
|
}
|
|
|
|
private static void HandleMouseWheel(AppState state, Vector2 mouse, bool overSidebar)
|
|
{
|
|
var wheel = Raylib.GetMouseWheelMove();
|
|
if (MathF.Abs(wheel) <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var settingsRect = GetSettingsRect(state);
|
|
if (state.SettingsVisible && Raylib.CheckCollisionPointRec(mouse, settingsRect))
|
|
{
|
|
var maxOffset = Math.Max(0, state.SettingItems.Length - GetVisibleSettingsRows(settingsRect));
|
|
state.SettingsScrollOffset = Math.Clamp(state.SettingsScrollOffset - (int)wheel, 0, maxOffset);
|
|
return;
|
|
}
|
|
|
|
var overCategoryList = overSidebar && mouse.Y >= 60 && mouse.Y < 240;
|
|
if (overCategoryList)
|
|
{
|
|
var maxOffset = Math.Max(0, state.Categories.Count - GetVisibleCategoryRows());
|
|
state.CategoryScrollOffset = Math.Clamp(state.CategoryScrollOffset - (int)wheel, 0, maxOffset);
|
|
return;
|
|
}
|
|
|
|
var overParticleList = overSidebar && mouse.Y >= 240 && mouse.Y <= Raylib.GetScreenHeight() - 164;
|
|
if (overParticleList)
|
|
{
|
|
var visibleRows = GetVisibleParticleRows();
|
|
var maxOffset = Math.Max(0, state.Categories[state.CurrentCategory].Count - visibleRows);
|
|
state.ParticleScrollOffset = Math.Clamp(state.ParticleScrollOffset - (int)wheel, 0, maxOffset);
|
|
return;
|
|
}
|
|
|
|
state.BrushRadius = Math.Clamp(state.BrushRadius + (int)wheel, 1, 16);
|
|
}
|
|
|
|
private static void HandleWorldPainting(AppState state, Vector2 mouse, Rectangle worldRect, bool overSidebar, Rectangle settingsRect)
|
|
{
|
|
var canPaintWorld =
|
|
!overSidebar &&
|
|
(!state.SettingsVisible || !Raylib.CheckCollisionPointRec(mouse, settingsRect)) &&
|
|
Raylib.CheckCollisionPointRec(mouse, worldRect);
|
|
|
|
state.PendingWorldAction = default;
|
|
|
|
if (Raylib.IsMouseButtonDown(MouseButton.Left) && canPaintWorld)
|
|
{
|
|
var simX = (int)((mouse.X - SidebarWidth) / ParticleSize) * ParticleSize;
|
|
var simY = (int)(mouse.Y / ParticleSize) * ParticleSize;
|
|
state.PendingWorldAction = new PendingWorldAction(true, false, simX, simY, mouse);
|
|
if (Raylib.IsMouseButtonPressed(MouseButton.Left))
|
|
{
|
|
ApplyWorldAction(state, state.PendingWorldAction, updateWindDirection: true);
|
|
}
|
|
}
|
|
|
|
if (Raylib.IsMouseButtonDown(MouseButton.Right) && canPaintWorld)
|
|
{
|
|
var simX = (int)((mouse.X - SidebarWidth) / ParticleSize) * ParticleSize;
|
|
var simY = (int)(mouse.Y / ParticleSize) * ParticleSize;
|
|
state.PendingWorldAction = new PendingWorldAction(true, true, simX, simY, mouse);
|
|
if (Raylib.IsMouseButtonPressed(MouseButton.Right))
|
|
{
|
|
ApplyWorldAction(state, state.PendingWorldAction, updateWindDirection: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ApplyWorldAction(AppState state, PendingWorldAction action, bool updateWindDirection, bool continuous = false)
|
|
{
|
|
if (!action.Active)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (action.IsErase)
|
|
{
|
|
state.Simulation.ClearParticlePourAtPixel(action.SimX, action.SimY, state.BrushRadius, GetContinuousEraseCount(state), state.Simulation.Frame);
|
|
return;
|
|
}
|
|
|
|
if (updateWindDirection && state.CurrentParticle is "wind" or "air")
|
|
{
|
|
var drag = action.Mouse - state.PreviousMouse;
|
|
if (drag.LengthSquared() > 0.25f)
|
|
{
|
|
state.LastWindDirection = Vector2.Normalize(drag);
|
|
}
|
|
}
|
|
|
|
switch (state.CurrentParticle)
|
|
{
|
|
case "wind":
|
|
state.Simulation.ApplyWindBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, state.LastWindDirection.X, state.LastWindDirection.Y);
|
|
break;
|
|
case "air":
|
|
state.Simulation.ApplyAirBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, state.LastWindDirection.X, state.LastWindDirection.Y);
|
|
break;
|
|
case "gravity_well":
|
|
state.Simulation.ApplyGravityBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, 3.5f);
|
|
break;
|
|
case "repulsor":
|
|
state.Simulation.ApplyRepulsorBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, 3f);
|
|
break;
|
|
default:
|
|
if (continuous)
|
|
{
|
|
var pourCount = GetContinuousPaintCount(state);
|
|
state.Simulation.CreateParticlePourAtPixel(action.SimX, action.SimY, state.BrushRadius, state.CurrentParticle, pourCount, state.Simulation.Frame);
|
|
}
|
|
else
|
|
{
|
|
state.Simulation.CreateParticleCircle(action.SimX, action.SimY, state.BrushRadius, state.CurrentParticle);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void HandleSidebarClick(AppState state, Vector2 mouse)
|
|
{
|
|
if (!Raylib.IsMouseButtonPressed(MouseButton.Left))
|
|
{
|
|
return;
|
|
}
|
|
|
|
HandleSidebar(state, mouse);
|
|
if (state.SettingsVisible)
|
|
{
|
|
HandleSettingsPanel(state, GetSettingsRect(state), mouse);
|
|
}
|
|
}
|
|
|
|
private static void StepSimulation(AppState state, float dt, Vector2 mouse)
|
|
{
|
|
var simStart = Stopwatch.GetTimestamp();
|
|
state.CategoryScrollOffset = Math.Clamp(state.CategoryScrollOffset, 0, Math.Max(0, state.Categories.Count - GetVisibleCategoryRows()));
|
|
state.ParticleScrollOffset = Math.Clamp(state.ParticleScrollOffset, 0, Math.Max(0, state.Categories[state.CurrentCategory].Count - GetVisibleParticleRows()));
|
|
state.SettingsScrollOffset = Math.Clamp(state.SettingsScrollOffset, 0, Math.Max(0, state.SettingItems.Length - GetVisibleSettingsRows(GetSettingsRect(state))));
|
|
state.LastSimulationStepCount = 0;
|
|
|
|
if (state.Settings.PauseSim)
|
|
{
|
|
if (state.PendingWorldAction.Active)
|
|
{
|
|
ApplyWorldAction(state, state.PendingWorldAction, updateWindDirection: true, continuous: ShouldUseContinuousPaint(state));
|
|
}
|
|
|
|
state.LastSimulationLoopTimeMicroseconds = ToMicroseconds(simStart, Stopwatch.GetTimestamp());
|
|
return;
|
|
}
|
|
|
|
state.SimulationAccumulator += dt;
|
|
var fixedStep = 1f / MathF.Max(1f, state.Settings.SimulationStepsPerSecond);
|
|
var overload = IsInteractiveOverloaded(state, fixedStep);
|
|
var maxSteps = overload ? 2 : 4;
|
|
while (state.SimulationAccumulator >= fixedStep && maxSteps-- > 0)
|
|
{
|
|
if (state.PendingWorldAction.Active)
|
|
{
|
|
ApplyWorldAction(state, state.PendingWorldAction with { Mouse = mouse }, updateWindDirection: true, continuous: ShouldUseContinuousPaint(state));
|
|
}
|
|
|
|
state.Simulation.Step(fixedStep * state.Settings.TimeScale);
|
|
state.LastSimulationStepCount++;
|
|
state.SimulationAccumulator -= fixedStep;
|
|
}
|
|
|
|
if (state.SimulationAccumulator > fixedStep * (overload ? 1.5f : 3f))
|
|
{
|
|
state.SimulationAccumulator = fixedStep;
|
|
}
|
|
|
|
state.LastSimulationLoopTimeMicroseconds = ToMicroseconds(simStart, Stopwatch.GetTimestamp());
|
|
}
|
|
|
|
private static Rectangle GetSettingsRect(AppState state)
|
|
{
|
|
return new Rectangle(
|
|
Raylib.GetScreenWidth() - SettingsWidth - 12,
|
|
100,
|
|
SettingsWidth,
|
|
MathF.Min(32 + (state.SettingItems.Length * 34), Raylib.GetScreenHeight() - 120));
|
|
}
|
|
|
|
private static int GetVisibleSettingsRows(Rectangle panelRect)
|
|
{
|
|
return Math.Max(1, (int)((panelRect.Height - 68f) / 34f));
|
|
}
|
|
|
|
private static bool ShouldUseContinuousPaint(AppState state)
|
|
{
|
|
if (state.CurrentParticle is "wind" or "air" or "gravity_well" or "repulsor")
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var typeId = state.Library.GetTypeId(state.CurrentParticle);
|
|
var particle = state.Library.GetDefinition(typeId);
|
|
return particle.Kind == ParticleKind.Gas
|
|
|| particle.Kind == ParticleKind.Liquid
|
|
|| (particle.Kind == ParticleKind.Solid && !particle.IsStatic);
|
|
}
|
|
|
|
private static int GetContinuousPaintCount(AppState state)
|
|
{
|
|
var typeId = state.Library.GetTypeId(state.CurrentParticle);
|
|
var particle = state.Library.GetDefinition(typeId);
|
|
var baseCount = GetContinuousPaintCount(particle, state.BrushRadius);
|
|
return ApplyInteractiveLoadScaling(state, baseCount, particle.Kind == ParticleKind.Gas);
|
|
}
|
|
|
|
internal static int GetContinuousPaintCount(ParticleDef particle, int brushRadius)
|
|
{
|
|
return particle.Kind switch
|
|
{
|
|
ParticleKind.Gas => Math.Clamp(brushRadius + 1, 1, 8),
|
|
ParticleKind.Liquid => Math.Clamp((brushRadius * 2) + 1, 1, 24),
|
|
ParticleKind.Solid when !particle.IsStatic => Math.Clamp((brushRadius * 2) + 1, 1, 24),
|
|
_ => 0,
|
|
};
|
|
}
|
|
|
|
private static int GetContinuousEraseCount(AppState state)
|
|
{
|
|
var baseCount = Math.Clamp((state.BrushRadius * 2) + 2, 1, 18);
|
|
return ApplyInteractiveLoadScaling(state, baseCount, gasBiased: true);
|
|
}
|
|
|
|
private static bool IsInteractiveOverloaded(AppState state, float fixedStep)
|
|
{
|
|
var stepBudgetMicros = fixedStep * 1_000_000f;
|
|
if (state.LastSimulationStepCount >= 4)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (state.LastSimulationLoopTimeMicroseconds > stepBudgetMicros * 2.25f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return state.LastAppFrameTimeMicroseconds > 40_000;
|
|
}
|
|
|
|
private static int ApplyInteractiveLoadScaling(AppState state, int baseCount, bool gasBiased)
|
|
{
|
|
if (baseCount <= 1)
|
|
{
|
|
return baseCount;
|
|
}
|
|
|
|
var severeOverload =
|
|
state.LastSimulationStepCount >= 4 ||
|
|
state.LastSimulationLoopTimeMicroseconds > 45_000 ||
|
|
state.LastAppFrameTimeMicroseconds > 60_000;
|
|
if (severeOverload)
|
|
{
|
|
return gasBiased
|
|
? Math.Max(1, baseCount / 4)
|
|
: Math.Max(1, baseCount / 3);
|
|
}
|
|
|
|
var overload =
|
|
state.LastSimulationStepCount >= 2 ||
|
|
state.LastSimulationLoopTimeMicroseconds > 25_000 ||
|
|
state.LastAppFrameTimeMicroseconds > 33_000;
|
|
if (overload)
|
|
{
|
|
return gasBiased
|
|
? Math.Max(1, baseCount / 2)
|
|
: Math.Max(1, (baseCount * 2) / 3);
|
|
}
|
|
|
|
return baseCount;
|
|
}
|
|
}
|