Improve Win2D editor UX
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
public enum EEditorTool
|
public enum EEditorTool
|
||||||
{
|
{
|
||||||
|
Cursor,
|
||||||
Floor,
|
Floor,
|
||||||
Wall,
|
Wall,
|
||||||
Reactor,
|
Reactor,
|
||||||
@@ -32,6 +33,7 @@ public static class LevelEditor
|
|||||||
|
|
||||||
var cell = level.GetCell(position);
|
var cell = level.GetCell(position);
|
||||||
cell = tool switch {
|
cell = tool switch {
|
||||||
|
EEditorTool.Cursor => cell,
|
||||||
EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
|
EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
|
||||||
EEditorTool.Wall => cell with {
|
EEditorTool.Wall => cell with {
|
||||||
Terrain = ECellTerrain.Wall,
|
Terrain = ECellTerrain.Wall,
|
||||||
|
|||||||
BIN
src/ReactorMaintenance.Win2D/Images/Props/cursor.png
Normal file
BIN
src/ReactorMaintenance.Win2D/Images/Props/cursor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/floor.png
Normal file
BIN
src/ReactorMaintenance.Win2D/Images/Props/floor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/ReactorMaintenance.Win2D/Images/Props/wall.png
Normal file
BIN
src/ReactorMaintenance.Win2D/Images/Props/wall.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -29,9 +29,11 @@
|
|||||||
<ScrollViewer Grid.Column="0" Background="#1C2126">
|
<ScrollViewer Grid.Column="0" Background="#1C2126">
|
||||||
<StackPanel Padding="12" Spacing="10">
|
<StackPanel Padding="12" Spacing="10">
|
||||||
<TextBlock Text="Tools" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
<TextBlock Text="Tools" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||||
<ComboBox x:Name="ToolPicker" SelectionChanged="ToolPicker_SelectionChanged">
|
<ItemsControl x:Name="ToolPicker">
|
||||||
<ComboBox.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
|
<RadioButton GroupName="EditorTools" IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||||
|
Checked="ToolRadio_Checked">
|
||||||
<Grid ColumnSpacing="8">
|
<Grid ColumnSpacing="8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="28" />
|
<ColumnDefinition Width="28" />
|
||||||
@@ -40,9 +42,10 @@
|
|||||||
<Image Width="24" Height="24" Source="{Binding Icon}" />
|
<Image Width="24" Height="24" Source="{Binding Icon}" />
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Label}" />
|
<TextBlock Grid.Column="1" Text="{Binding Label}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</RadioButton>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ComboBox.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ComboBox>
|
</ItemsControl>
|
||||||
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" />
|
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||||
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE"
|
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
@@ -57,7 +60,8 @@
|
|||||||
Draw="LevelCanvas_Draw"
|
Draw="LevelCanvas_Draw"
|
||||||
PointerPressed="LevelCanvas_PointerPressed"
|
PointerPressed="LevelCanvas_PointerPressed"
|
||||||
PointerMoved="LevelCanvas_PointerMoved"
|
PointerMoved="LevelCanvas_PointerMoved"
|
||||||
PointerReleased="LevelCanvas_PointerReleased" />
|
PointerReleased="LevelCanvas_PointerReleased"
|
||||||
|
PointerWheelChanged="LevelCanvas_PointerWheelChanged" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ScrollViewer Grid.Column="2" Background="#1C2126">
|
<ScrollViewer Grid.Column="2" Background="#1C2126">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Microsoft.Graphics.Canvas.UI;
|
using Microsoft.Graphics.Canvas.UI;
|
||||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||||
using Microsoft.UI;
|
using Microsoft.UI;
|
||||||
|
using Microsoft.UI.Input;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
@@ -11,7 +12,9 @@ using System.Globalization;
|
|||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
using Windows.Storage.Pickers;
|
using Windows.Storage.Pickers;
|
||||||
|
using Windows.System;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
|
using Windows.UI.Core;
|
||||||
using Windows.UI.Popups;
|
using Windows.UI.Popups;
|
||||||
using WinRT.Interop;
|
using WinRT.Interop;
|
||||||
|
|
||||||
@@ -34,16 +37,21 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private sealed record ForecastViewModel(BitmapImage Icon, string Message);
|
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()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
m_Level = BuildStarterLevel();
|
m_Level = BuildStarterLevel();
|
||||||
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString())).ToArray();
|
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray();
|
||||||
ToolPicker.ItemsSource = m_EditorTools;
|
ToolPicker.ItemsSource = m_EditorTools;
|
||||||
ToolPicker.SelectedItem = m_EditorTools.First(tool => tool.Tool == m_SelectedTool);
|
|
||||||
RefreshInspector();
|
RefreshInspector();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +86,14 @@ public sealed partial class MainWindow
|
|||||||
return await CanvasBitmap.LoadAsync(sender, path);
|
return await CanvasBitmap.LoadAsync(sender, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToolPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void ToolRadio_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool)
|
||||||
{
|
{
|
||||||
if (ToolPicker.SelectedItem is EditorToolViewModel tool)
|
|
||||||
m_SelectedTool = tool.Tool;
|
m_SelectedTool = tool.Tool;
|
||||||
|
foreach (var editorTool in m_EditorTools)
|
||||||
|
editorTool.IsSelected = editorTool == tool;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void New_Click(object sender, RoutedEventArgs e)
|
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)
|
private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
m_Painting = true;
|
var point = e.GetCurrentPoint(LevelCanvas);
|
||||||
|
if (point.Properties.IsLeftButtonPressed && IsShiftDown())
|
||||||
|
{
|
||||||
|
m_Panning = true;
|
||||||
|
m_LastPanPoint = point.Position;
|
||||||
_ = LevelCanvas.CapturePointer(e.Pointer);
|
_ = LevelCanvas.CapturePointer(e.Pointer);
|
||||||
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
|
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)
|
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)
|
if (m_Painting)
|
||||||
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
|
{
|
||||||
|
PaintAt(point.Position);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
|
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
m_Painting = false;
|
m_Painting = false;
|
||||||
|
m_Panning = false;
|
||||||
LevelCanvas.ReleasePointerCapture(e.Pointer);
|
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)
|
private void PaintAt(Point point)
|
||||||
@@ -417,14 +531,47 @@ public sealed partial class MainWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CanvasLayout GetLayout()
|
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 availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
|
||||||
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
|
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
|
||||||
var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height));
|
var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height));
|
||||||
size = Math.Max(20, size);
|
return 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);
|
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()
|
private void RefreshInspector()
|
||||||
@@ -465,7 +612,9 @@ public sealed partial class MainWindow
|
|||||||
private static BitmapImage? EditorToolIcon(EEditorTool tool)
|
private static BitmapImage? EditorToolIcon(EEditorTool tool)
|
||||||
{
|
{
|
||||||
return tool switch {
|
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.Reactor => PropImage("reactor.png"),
|
||||||
EEditorTool.CoolingPump => PropImage("cooling-pump.png"),
|
EEditorTool.CoolingPump => PropImage("cooling-pump.png"),
|
||||||
EEditorTool.Generator => PropImage("generator.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_EastConnection = 2;
|
||||||
private const int c_SouthConnection = 4;
|
private const int c_SouthConnection = 4;
|
||||||
private const int c_WestConnection = 8;
|
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 SimulationEngine m_Simulation = new();
|
||||||
private readonly Dictionary<ECellProp, CanvasBitmap> m_PropSprites = [];
|
private readonly Dictionary<ECellProp, CanvasBitmap> m_PropSprites = [];
|
||||||
@@ -571,8 +723,13 @@ public sealed partial class MainWindow
|
|||||||
private LevelState m_Level;
|
private LevelState m_Level;
|
||||||
private IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
|
private IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
|
||||||
private bool m_Painting;
|
private bool m_Painting;
|
||||||
|
private bool m_Panning;
|
||||||
|
private Point m_LastPanPoint;
|
||||||
private GridPosition? m_SelectedCell;
|
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_TerrainTilemap;
|
||||||
private CanvasBitmap? m_RobotSprite;
|
private CanvasBitmap? m_RobotSprite;
|
||||||
private CanvasBitmap? m_LeakSprite;
|
private CanvasBitmap? m_LeakSprite;
|
||||||
|
|||||||
Reference in New Issue
Block a user