using System.IO; public partial class Terrain { enum ToolMode { Height, Flatten, Paint } enum ToolShape { Corner, Circle, Square } // Set by the terrain editing plugin. public EditorUndoRedoManager EditorUndoRedo { get; set; } Material _editToolMaterial; public override void _EnterTree() { _editToolMaterial = new StandardMaterial3D { AlbedoColor = Colors.Blue, ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, NoDepthTest = true, }; } public bool EditorInput(InputEventMouse ev, Vector3 position, Control controls) { var prevent_default = false; var toolMode = (ToolMode)(int)controls.Get("tool_mode"); var toolShape = (ToolShape)(int)controls.Get("tool_shape"); var texture = (int)controls.Get("texture"); var drawSize = (int)controls.Get("draw_size"); var isRaise = (bool)controls.Get("is_raise"); var isConnected = (bool)controls.Get("is_connected"); var isFlatten = toolMode == ToolMode.Flatten; var isCorner = toolShape == ToolShape.Corner; var hover = ToTilePos(position); if (isCorner) drawSize = 1; var isEven = (drawSize % 2) == 0; var radius = FloorToInt(drawSize / 2.0f); // Offset hover tile position by corner. if (isEven) hover.Position = hover.Corner switch { Corner.TopLeft => hover.Position.Offset(0, 0), Corner.TopRight => hover.Position.Offset(1, 0), Corner.BottomRight => hover.Position.Offset(1, 1), Corner.BottomLeft => hover.Position.Offset(0, 1), _ => throw new InvalidOperationException(), }; IEnumerable GetTilesInSquare() { var min = hover.Position.Offset(-radius, -radius); var max = hover.Position.Offset(+radius, +radius); if (isEven) max = max.Offset(-1, -1); for (var x = min.X; x <= max.X; x++) for (var y = min.Y; y <= max.Y; y++) yield return new(x, y); } IEnumerable GetTilesInRadius() { var center = isEven ? hover.Position.ToVector2I() : hover.Position.ToCenter(); var distanceSqr = Pow(radius + 0.25f * (isEven ? -1 : 1), 2); return GetTilesInSquare().Where(tile => center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr); } var tiles = (toolShape switch { ToolShape.Corner => [ hover.Position ], ToolShape.Circle => GetTilesInRadius(), ToolShape.Square => GetTilesInSquare(), _ => throw new InvalidOperationException(), }).ToHashSet(); // TODO: Handle different tool modes, such as painting. // TODO: Finally allow editing single corners. // TODO: Allow click-dragging which doesn't affect already changed tiles / corners. // TODO: Make mesh generation generate vertical walls between disconnected corners. // TODO: Use ArrayMesh instead of ImmediateMesh. // TODO: Dynamically expand terrain instead of having it be a set size. // Raise / lower the terrain when left mouse button is pressed. if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { prevent_default = true; 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 (isConnected) { // 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. foreach (var pos in tiles) { var tile = GetTile(pos); foreach (var corner in Enum.GetValues()) { var height = tile.Height[corner]; foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner)) { if (tiles.Contains(neighborPos)) continue; var neighborHeight = GetTile(neighborPos).Height[neighborCorner]; if (neighborHeight == height) cornersToChange.Add((neighborPos, neighborCorner)); } } } } const float AdjustHeight = 0.5f; var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] : isRaise ? 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 tile = GetTile(pos); tilesPrevious.Add((pos, tile.Height)); var newHeight = tile.Height; foreach (var corner in group) { if (isFlatten) newHeight[corner] = amount; else newHeight[corner] += amount; } tilesChanged.Add((pos, newHeight)); } if (EditorUndoRedo is EditorUndoRedoManager undo) { var name = "Modify terrain height"; // TODO: Change name depending on tool mode. undo.CreateAction(name, backwardUndoOps: false); undo.AddDoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesChanged)); undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); undo.AddUndoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesPrevious)); undo.AddUndoMethod(this, nameof(UpdateMeshAndShape)); undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); undo.CommitAction(true); } } UpdateEditToolMesh(tiles); return prevent_default; } public void EditorUnfocus() => ClearEditToolMesh(); public void DoModifyTerrainHeight(byte[] data) { foreach (var (pos, corners) in Unpack(data)) { var tile = GetTile(pos); tile.Height = corners; SetTile(pos, tile); } } void UpdateEditToolMesh(IEnumerable tiles) { var mesh = GetOrCreateMesh("EditToolMesh"); mesh.ClearSurfaces(); mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); void AddLine(Vector3 start, Vector3 end) { mesh.SurfaceAddVertex(start); mesh.SurfaceAddVertex(end); } void AddQuad(Vector3 topLeft , Vector3 topRight , Vector3 bottomRight, Vector3 bottomLeft) { AddLine(topLeft , topRight ); AddLine(topRight , bottomRight); AddLine(bottomRight, bottomLeft ); AddLine(bottomLeft , topLeft ); } foreach (var tile in tiles) { var (topLeft, topRight, bottomRight, bottomLeft) = GetTileCornerPositions(tile); AddQuad(topLeft, topRight, bottomRight, bottomLeft); } mesh.SurfaceEnd(); mesh.SurfaceSetMaterial(0, _editToolMaterial); } void ClearEditToolMesh() => GetNodeOrNull("EditToolMesh")?.QueueFree(); (TilePos Position, Corner Corner) ToTilePos(Vector3 position) { var local = ToLocal(position); var coord = new Vector2(local.X, local.Z) / TileSize + (Size + Vector2.One) / 2; var pos = TilePos.From(coord); var corner = coord.PosMod(1).RoundToVector2I() switch { (0, 0) => Corner.TopLeft, (1, 0) => Corner.TopRight, (1, 1) => Corner.BottomRight, (0, 1) => Corner.BottomLeft, _ => throw new InvalidOperationException(), }; return (pos, corner); } static readonly Dictionary _offsetLookup = new(){ [Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )], [Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)], [Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )], [Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )], }; static IEnumerable<(TilePos, Corner)> GetNeighbors(TilePos pos, Corner corner) => _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite)); 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); } } }