diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs index 4a1e63f..9daaea9 100644 --- a/src/ReactorMaintenance.Simulation/LevelEditor.cs +++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs @@ -2,6 +2,7 @@ public enum EEditorTool { + Cursor, Floor, Wall, Reactor, @@ -32,6 +33,7 @@ public static class LevelEditor var cell = level.GetCell(position); cell = tool switch { + EEditorTool.Cursor => cell, EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor }, EEditorTool.Wall => cell with { Terrain = ECellTerrain.Wall, diff --git a/src/ReactorMaintenance.Win2D/Images/Props/cursor.png b/src/ReactorMaintenance.Win2D/Images/Props/cursor.png new file mode 100644 index 0000000..30e99e0 Binary files /dev/null and b/src/ReactorMaintenance.Win2D/Images/Props/cursor.png differ diff --git a/src/ReactorMaintenance.Win2D/Images/Props/floor.png b/src/ReactorMaintenance.Win2D/Images/Props/floor.png new file mode 100644 index 0000000..766913f Binary files /dev/null and b/src/ReactorMaintenance.Win2D/Images/Props/floor.png differ diff --git a/src/ReactorMaintenance.Win2D/Images/Props/wall.png b/src/ReactorMaintenance.Win2D/Images/Props/wall.png new file mode 100644 index 0000000..a13d82a Binary files /dev/null and b/src/ReactorMaintenance.Win2D/Images/Props/wall.png differ diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml b/src/ReactorMaintenance.Win2D/MainWindow.xaml index 904fe3a..bb45e54 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml @@ -29,20 +29,23 @@ - - + + - - - - - - - - + + + + + + + + + + - - + + @@ -57,7 +60,8 @@ Draw="LevelCanvas_Draw" PointerPressed="LevelCanvas_PointerPressed" PointerMoved="LevelCanvas_PointerMoved" - PointerReleased="LevelCanvas_PointerReleased" /> + PointerReleased="LevelCanvas_PointerReleased" + PointerWheelChanged="LevelCanvas_PointerWheelChanged" /> diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs index fc0728e..f961752 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs @@ -2,6 +2,7 @@ using Microsoft.Graphics.Canvas.UI; using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.UI; +using Microsoft.UI.Input; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; @@ -11,7 +12,9 @@ using System.Globalization; using Windows.Foundation; using Windows.Storage; using Windows.Storage.Pickers; +using Windows.System; using Windows.UI; +using Windows.UI.Core; using Windows.UI.Popups; using WinRT.Interop; @@ -34,16 +37,21 @@ public sealed partial class MainWindow private sealed record ForecastViewModel(BitmapImage Icon, string Message); - private sealed record EditorToolViewModel(EEditorTool Tool, BitmapImage? Icon, string Label); + private sealed class EditorToolViewModel(EEditorTool tool, BitmapImage? icon, string label) + { + public EEditorTool Tool { get; } = tool; + public BitmapImage? Icon { get; } = icon; + public string Label { get; } = label; + public bool IsSelected { get; set; } + } public MainWindow() { InitializeComponent(); m_Level = BuildStarterLevel(); - m_EditorTools = Enum.GetValues().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString())).ToArray(); + m_EditorTools = Enum.GetValues().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray(); ToolPicker.ItemsSource = m_EditorTools; - ToolPicker.SelectedItem = m_EditorTools.First(tool => tool.Tool == m_SelectedTool); RefreshInspector(); } @@ -78,10 +86,14 @@ public sealed partial class MainWindow return await CanvasBitmap.LoadAsync(sender, path); } - private void ToolPicker_SelectionChanged(object sender, SelectionChangedEventArgs e) + private void ToolRadio_Checked(object sender, RoutedEventArgs e) { - if (ToolPicker.SelectedItem is EditorToolViewModel tool) + if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool) + { m_SelectedTool = tool.Tool; + foreach (var editorTool in m_EditorTools) + editorTool.IsSelected = editorTool == tool; + } } private void New_Click(object sender, RoutedEventArgs e) @@ -163,21 +175,123 @@ public sealed partial class MainWindow private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e) { - m_Painting = true; - _ = LevelCanvas.CapturePointer(e.Pointer); - PaintAt(e.GetCurrentPoint(LevelCanvas).Position); + var point = e.GetCurrentPoint(LevelCanvas); + if (point.Properties.IsLeftButtonPressed && IsShiftDown()) + { + m_Panning = true; + m_LastPanPoint = point.Position; + _ = LevelCanvas.CapturePointer(e.Pointer); + e.Handled = true; + return; + } + + if (point.Properties.IsRightButtonPressed) + { + RemovePropAt(point.Position); + e.Handled = true; + return; + } + + if (point.Properties.IsLeftButtonPressed) + { + _ = LevelCanvas.CapturePointer(e.Pointer); + SelectOrPaintAt(point.Position); + m_Painting = m_SelectedTool != EEditorTool.Cursor; + e.Handled = true; + } } private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e) { + var point = e.GetCurrentPoint(LevelCanvas); + if (m_Panning) + { + var deltaX = point.Position.X - m_LastPanPoint.X; + var deltaY = point.Position.Y - m_LastPanPoint.Y; + m_LastPanPoint = point.Position; + m_PanX += deltaX; + m_PanY += deltaY; + ClampPan(); + LevelCanvas.Invalidate(); + e.Handled = true; + return; + } + if (m_Painting) - PaintAt(e.GetCurrentPoint(LevelCanvas).Position); + { + PaintAt(point.Position); + e.Handled = true; + } } private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e) { m_Painting = false; + m_Panning = false; LevelCanvas.ReleasePointerCapture(e.Pointer); + e.Handled = true; + } + + private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e) + { + var point = e.GetCurrentPoint(LevelCanvas); + var wheelDelta = point.Properties.MouseWheelDelta; + if (wheelDelta == 0) + return; + + ZoomAt(point.Position, wheelDelta > 0 ? c_ZoomStep : 1 / c_ZoomStep); + e.Handled = true; + } + + private static bool IsShiftDown() + { + return InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); + } + + private void ZoomAt(Point point, double zoomFactor) + { + var oldLayout = GetLayout(); + var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize; + var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize; + + m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom); + var newCellSize = GetBaseCellSize() * m_Zoom; + var originWithoutPan = GetCenteredOrigin(newCellSize); + m_PanX = point.X - originWithoutPan.X - (cellX * newCellSize); + m_PanY = point.Y - originWithoutPan.Y - (cellY * newCellSize); + ClampPan(); + LevelCanvas.Invalidate(); + } + + private void SelectOrPaintAt(Point point) + { + if (m_SelectedTool == EEditorTool.Cursor) + SelectAt(point); + else + PaintAt(point); + } + + private void SelectAt(Point point) + { + if (!TryGetGridPosition(point, out var position)) + return; + + m_SelectedCell = position; + RefreshInspector(); + LevelCanvas.Invalidate(); + } + + private void RemovePropAt(Point point) + { + if (!TryGetGridPosition(point, out var position)) + return; + + var cell = m_Level.GetCell(position); + m_SelectedCell = position; + m_Level = m_Level.SetCell(position, cell with { Prop = ECellProp.None }); + m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) }; + RefreshInspector(); + LevelCanvas.Invalidate(); } private void PaintAt(Point point) @@ -417,14 +531,47 @@ public sealed partial class MainWindow } private CanvasLayout GetLayout() + { + ClampPan(); + var cellSize = GetBaseCellSize() * m_Zoom; + var centeredOrigin = GetCenteredOrigin(cellSize); + return new(cellSize, centeredOrigin.X + m_PanX, centeredOrigin.Y + m_PanY); + } + + private double GetBaseCellSize() { var availableWidth = Math.Max(1, LevelCanvas.ActualWidth); var availableHeight = Math.Max(1, LevelCanvas.ActualHeight); var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height)); - size = Math.Max(20, size); - var originX = Math.Max(0, (availableWidth - (size * m_Level.Width)) / 2); - var originY = Math.Max(0, (availableHeight - (size * m_Level.Height)) / 2); - return new(size, originX, originY); + return Math.Max(20, size); + } + + private Point GetCenteredOrigin(double cellSize) + { + var availableWidth = Math.Max(1, LevelCanvas.ActualWidth); + var availableHeight = Math.Max(1, LevelCanvas.ActualHeight); + return new((availableWidth - (cellSize * m_Level.Width)) / 2, (availableHeight - (cellSize * m_Level.Height)) / 2); + } + + private void ClampPan() + { + var cellSize = GetBaseCellSize() * m_Zoom; + var contentWidth = cellSize * m_Level.Width; + var contentHeight = cellSize * m_Level.Height; + var availableWidth = Math.Max(1, LevelCanvas.ActualWidth); + var availableHeight = Math.Max(1, LevelCanvas.ActualHeight); + + m_PanX = ClampAxisPan(m_PanX, contentWidth, availableWidth); + m_PanY = ClampAxisPan(m_PanY, contentHeight, availableHeight); + } + + private static double ClampAxisPan(double pan, double contentSize, double availableSize) + { + if (contentSize <= availableSize) + return 0; + + var maxPan = (contentSize - availableSize) / 2; + return Math.Clamp(pan, -maxPan, maxPan); } private void RefreshInspector() @@ -465,7 +612,9 @@ public sealed partial class MainWindow private static BitmapImage? EditorToolIcon(EEditorTool tool) { return tool switch { - EEditorTool.Floor or EEditorTool.Wall => null, + EEditorTool.Cursor => PropImage("cursor.png"), + EEditorTool.Floor => PropImage("floor.png"), + EEditorTool.Wall => PropImage("wall.png"), EEditorTool.Reactor => PropImage("reactor.png"), EEditorTool.CoolingPump => PropImage("cooling-pump.png"), EEditorTool.Generator => PropImage("generator.png"), @@ -563,6 +712,9 @@ public sealed partial class MainWindow private const int c_EastConnection = 2; private const int c_SouthConnection = 4; private const int c_WestConnection = 8; + private const double c_MinZoom = 0.5; + private const double c_MaxZoom = 4; + private const double c_ZoomStep = 1.15; private readonly SimulationEngine m_Simulation = new(); private readonly Dictionary m_PropSprites = []; @@ -571,8 +723,13 @@ public sealed partial class MainWindow private LevelState m_Level; private IReadOnlyList m_EditorTools = []; private bool m_Painting; + private bool m_Panning; + private Point m_LastPanPoint; private GridPosition? m_SelectedCell; - private EEditorTool m_SelectedTool = EEditorTool.Floor; + private EEditorTool m_SelectedTool = EEditorTool.Cursor; + private double m_Zoom = 1; + private double m_PanX; + private double m_PanY; private CanvasBitmap? m_TerrainTilemap; private CanvasBitmap? m_RobotSprite; private CanvasBitmap? m_LeakSprite;