Implement undo / redo for terrain editing

main
copygirl 3 weeks ago
parent be477542b8
commit 2e5edf092c
  1. 1
      addons/terrain-editing/terrain_editing_plugin.gd
  2. 3
      terrain/Tile.cs
  3. 101
      terrain/editing/TerrainEditingControls+Editing.cs

@ -20,6 +20,7 @@ func _make_visible(visible: bool) -> void:
if not controls:
var controls_scene = load("res://terrain/editing/TerrainEditingControls.tscn")
controls = controls_scene.instantiate()
controls.EditorUndoRedo = get_undo_redo()
add_control_to_container(CONTAINER, controls)
elif controls and controls.get_parent():
remove_control_from_container(CONTAINER, controls)

@ -90,6 +90,9 @@ public struct Corners<T>(T topLeft, T topRight, T bottomRight, T bottomLeft)
public T BottomRight = bottomRight;
public T BottomLeft = bottomLeft;
public Corners(T value)
: this(value, value, value, value) { }
public T this[Corner corner] {
readonly get => corner switch {
Corner.TopLeft => TopLeft,

@ -1,5 +1,10 @@
using System.IO;
public partial class TerrainEditingControls
{
// Set by the terrain editing plugin.
public EditorUndoRedoManager EditorUndoRedo { get; set; }
Terrain _currentTerrain = null;
Material _editToolMaterial;
@ -76,7 +81,6 @@ public partial class TerrainEditingControls
// TODO: Handle different tool modes, such as flatten and painting.
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
// TODO: Support undo and redo.
// TODO: Make mesh generation generate vertical walls between disconnected corners.
// TODO: Use ArrayMesh instead of ImmediateMesh.
@ -84,13 +88,16 @@ public partial class TerrainEditingControls
if (mouse is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
GetViewport().SetInputAsHandled();
const float AdjustHeight = 0.5f;
var amount = UpDownToggle.ButtonPressed ? AdjustHeight : -AdjustHeight;
var cornersToChange = new HashSet<(TilePos Position, Corner Corner)>();
// Raise selected tiles themselves.
foreach (var pos in tiles)
foreach (var corner2 in Enum.GetValues<Corner>())
cornersToChange.Add((pos, corner2));
// If the Connected toggle button is active, move "connected" corners.
// Connected corners are the ones that are at the same height as ones already being moved.
if (ConnectedToggle.ButtonPressed) {
var corners = new HashSet<(TilePos Position, Corner Corner)>();
foreach (var pos in tiles) {
var tile2 = terrain.GetTile(pos);
foreach (var corner2 in Enum.GetValues<Corner>()) {
@ -98,30 +105,47 @@ public partial class TerrainEditingControls
foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner2)) {
if (tiles.Contains(neighborPos)) continue;
var neighborHeight = terrain.GetTile(neighborPos).Height[neighborCorner];
if (neighborHeight == height) corners.Add((neighborPos, neighborCorner));
if (neighborHeight == height) cornersToChange.Add((neighborPos, neighborCorner));
}
}
}
// Raise connected corners.
foreach (var group in corners.GroupBy(e => e.Position, e => e.Corner)) {
var pos = group.Key;
var tile2 = terrain.GetTile(pos);
foreach (var corner2 in group)
tile2.Height[corner2] += amount;
terrain.SetTile(pos, tile2);
}
}
// Raise selected tiles themselves.
foreach (var pos in tiles) {
const float AdjustHeight = 0.5f;
var flatten = ToolMode == ToolMode.Flatten;
var amount = flatten ? terrain.GetTile(tile).Height[corner]
: UpDownToggle.ButtonPressed ? +AdjustHeight
: -AdjustHeight;
var tilesPrevious = new List<(TilePos, Corners<float>)>();
var tilesChanged = new List<(TilePos, Corners<float>)>();
foreach (var group in cornersToChange.GroupBy(c => c.Position, c => c.Corner)) {
var pos = group.Key;
var tile2 = terrain.GetTile(pos);
tile2.Height.Adjust(amount);
terrain.SetTile(pos, tile2);
tilesPrevious.Add((pos, tile2.Height));
var newHeight = tile2.Height;
foreach (var corner2 in group) {
if (flatten) newHeight[corner2] = amount;
else newHeight[corner2] += amount;
}
tilesChanged.Add((pos, newHeight));
}
terrain.UpdateMeshAndShape();
terrain.NotifyPropertyListChanged();
if (EditorUndoRedo is EditorUndoRedoManager undo) {
var name = "Modify terrain"; // TODO: Change name depending on tool mode.
undo.CreateAction(name, customContext: terrain, backwardUndoOps: false);
undo.AddDoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesChanged));
undo.AddDoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged);
undo.AddDoMethod(terrain, nameof(Terrain.UpdateMeshAndShape));
undo.AddUndoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesPrevious));
undo.AddUndoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged);
undo.AddUndoMethod(terrain, nameof(Terrain.UpdateMeshAndShape));
undo.CommitAction(true);
}
}
UpdateEditToolMesh(terrain, tiles);
@ -131,6 +155,43 @@ public partial class TerrainEditingControls
}
}
public void TerrainModifyHeight(Terrain terrain, byte[] data)
{
foreach (var (pos, corners) in Unpack(data)) {
var tile = terrain.GetTile(pos);
tile.Height = corners;
terrain.SetTile(pos, tile);
}
}
static byte[] Pack(IEnumerable<(TilePos Position, Corners<float> Corners)> data) {
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
foreach (var (pos, corners) in data) {
writer.Write(pos.X);
writer.Write(pos.Y);
writer.Write(corners.TopLeft);
writer.Write(corners.TopRight);
writer.Write(corners.BottomRight);
writer.Write(corners.BottomLeft);
}
return stream.ToArray();
}
static IEnumerable<(TilePos Position, Corners<float> Corners)> Unpack(byte[] data)
{
using var stream = new MemoryStream(data);
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length) {
var x = reader.ReadInt32();
var y = reader.ReadInt32();
var corners = new Corners<float>(
reader.ReadSingle(), reader.ReadSingle(),
reader.ReadSingle(), reader.ReadSingle());
yield return (new(x, y), corners);
}
}
void UpdateEditToolMesh(Terrain terrain, IEnumerable<TilePos> tiles)
{
if (terrain != _currentTerrain) ClearEditToolMesh();

Loading…
Cancel
Save