diff --git a/addons/terrain-editing/terrain_editing_plugin.gd b/addons/terrain-editing/terrain_editing_plugin.gd index 54d8cf8..979fe63 100644 --- a/addons/terrain-editing/terrain_editing_plugin.gd +++ b/addons/terrain-editing/terrain_editing_plugin.gd @@ -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) diff --git a/terrain/Tile.cs b/terrain/Tile.cs index 994a081..4523288 100644 --- a/terrain/Tile.cs +++ b/terrain/Tile.cs @@ -90,6 +90,9 @@ public struct Corners(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, diff --git a/terrain/editing/TerrainEditingControls+Editing.cs b/terrain/editing/TerrainEditingControls+Editing.cs index 3539871..1da3c38 100644 --- a/terrain/editing/TerrainEditingControls+Editing.cs +++ b/terrain/editing/TerrainEditingControls+Editing.cs @@ -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()) + 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()) { @@ -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)>(); + var tilesChanged = new List<(TilePos, Corners)>(); + 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 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 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( + reader.ReadSingle(), reader.ReadSingle(), + reader.ReadSingle(), reader.ReadSingle()); + yield return (new(x, y), corners); + } + } + void UpdateEditToolMesh(Terrain terrain, IEnumerable tiles) { if (terrain != _currentTerrain) ClearEditToolMesh();