[Tool] public partial class Terrain { bool _unhandledMouseMotion = false; // Used to detect when mouse moves off the terrain. TilePos? _tileHover = null; // Position of currently hovered tile. bool _isSelecting = false; // Whether left mouse is held down to select tiles. (TilePos, TilePos)? _selection = null; public override void _Input(InputEvent ev) { if (!IsEditing()) return; if (_isSelecting && ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false }) { _isSelecting = false; GetViewport().SetInputAsHandled(); } if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true }) && (wheel is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null)) { // NOTE: Potential bug in the Godot editor? // Does it zoom both when mouse wheel is "pressed" and "released"? // Because just cancelling one of them still causes zooming to occur. GetViewport().SetInputAsHandled(); if (!pressed) return; const float AdjustHeight = 0.5f; var amount = (wheel == MouseButton.WheelUp) ? AdjustHeight : -AdjustHeight; var selection = TileRegion.From(_selection.Value); // Raise connected corners. foreach (var innerCorner in Enum.GetValues()) { var outerCorner = innerCorner.GetOpposite(); var innerPos = selection.GetTileFor(innerCorner); var outerPos = innerPos.GetNeighbor(innerCorner); var innerHeight = GetCornerHeights(innerPos)[innerCorner]; var outerHeight = GetCornerHeights(outerPos)[outerCorner]; if (IsEqualApprox(outerHeight, innerHeight)) SetCornerHeight(outerPos, outerCorner, innerHeight + amount); } // Raise connected sides. foreach (var side in Enum.GetValues()) { foreach (var innerPos in selection.GetTilesFor(side)) { var outerPos = innerPos.GetNeighbor(side); var innerHeights = GetCornerHeights(innerPos); var outerHeights = GetCornerHeights(outerPos); var (innerCorner1, innerCorner2) = side.GetCorners(); var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners(); var current = outerHeights; var changed = false; var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) }; foreach (var (innerCorner, outerCorner) in matchingCorners) { var innerHeight = innerHeights[innerCorner]; var outerHeight = outerHeights[outerCorner]; if (IsEqualApprox(outerHeight, innerHeight)) { current = current.With(outerCorner, innerHeight + amount); changed = true; } } if (changed) SetCornerHeights(outerPos, current); } } // Raise selected tiles themselves. foreach (var tile in selection.GetAllTiles()) AdjustTileHeight(tile, amount); UpdateMeshAndShape(); NotifyPropertyListChanged(); } if (ev is InputEventMouseMotion) _unhandledMouseMotion = true; } public override void _InputEvent(Camera3D camera, InputEvent ev, Vector3 position, Vector3 normal, int shapeIdx) { if (!IsEditing()) return; var localPos = ToLocal(position); var tilePos = ToTilePos(localPos); if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { _selection = new(tilePos, tilePos); _isSelecting = true; GetViewport().SetInputAsHandled(); } if (ev is InputEventMouseMotion) { _unhandledMouseMotion = false; _tileHover = tilePos; if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos }; } } public override void _Process(double delta) { if (!IsEditing()) { _tileHover = null; _selection = null; _isSelecting = false; } if (_unhandledMouseMotion) _tileHover = null; if ((_tileHover != null) || (_selection != null)) { 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 bottomLeft, Vector3 bottomRight) { AddLine(topLeft , topRight ); AddLine(topRight , bottomRight); AddLine(bottomRight, bottomLeft ); AddLine(bottomLeft , topLeft ); } if (_tileHover is TilePos hover) { var corners = GetCornersPosition(hover); var margin = 0.1f; mesh.SurfaceSetColor(Colors.Black); AddQuad(corners.TopLeft + new Vector3(-margin, 0, -margin), corners.TopRight + new Vector3(+margin, 0, -margin), corners.BottomLeft + new Vector3(-margin, 0, +margin), corners.BottomRight + new Vector3(+margin, 0, +margin)); } mesh.SurfaceSetColor(Colors.Blue); if (_selection is (TilePos, TilePos) selection) foreach (var pos in TileRegion.From(selection).GetAllTiles()) { var corners = GetCornersPosition(pos); AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight); } mesh.SurfaceEnd(); mesh.SurfaceSetMaterial(0, _editToolMaterial); } else { var meshInstance = (MeshInstance3D)GetNodeOrNull("EditToolMesh"); var mesh = (ImmediateMesh)meshInstance?.Mesh; mesh?.ClearSurfaces(); } } bool IsEditing() { if (Engine.IsEditorHint()) { var selection = EditorInterface.Singleton.GetSelection(); return selection.GetSelectedNodes().Contains(this); } else { return false; } } readonly record struct TileRegion(int Left, int Top, int Right, int Bottom) { public TilePos TopLeft => new(Left , Top); public TilePos TopRight => new(Right, Top); public TilePos BottomRight => new(Right, Bottom); public TilePos BottomLeft => new(Left , Bottom); public int Width => Right - Left + 1; public int Height => Bottom - Top + 1; public static TileRegion From((TilePos, TilePos) selection) => From(selection.Item1, selection.Item2); public static TileRegion From(TilePos a, TilePos b) => new(Min(a.X, b.X), Min(a.Y, b.Y), Max(a.X, b.X), Max(a.Y, b.Y)); public TilePos GetTileFor(Corner corner) => corner switch { Corner.TopLeft => TopLeft, Corner.TopRight => TopRight, Corner.BottomRight => BottomRight, Corner.BottomLeft => BottomLeft, _ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)), }; public IEnumerable GetTilesFor(Side side) { var (left, top, right, bottom) = this; return side switch { Side.Left => Enumerable.Range(Top, Height).Select(y => new TilePos(left, y)), Side.Top => Enumerable.Range(Left, Width).Select(x => new TilePos(x, top)), Side.Right => Enumerable.Range(Top, Height).Select(y => new TilePos(right, y)), Side.Bottom => Enumerable.Range(Left, Width).Select(x => new TilePos(x, bottom)), _ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)), }; } public IEnumerable GetAllTiles() { for (var x = Left; x <= Right; x++) for (var y = Top; y <= Bottom; y++) yield return new(x, y); } } }