From f7a9dfa2f1d583a3e78b2ae1a0093f9ea7363dea Mon Sep 17 00:00:00 2001 From: copygirl Date: Mon, 16 Sep 2024 12:07:22 +0200 Subject: [PATCH] Visualize to-be-drawn shape --- .../TerrainEditingControls+Editing.cs | 428 +++++++++--------- .../TerrainEditingControls.tscn | 26 +- terrain/Terrain.cs | 5 - terrain/Tile.cs | 10 + 4 files changed, 237 insertions(+), 232 deletions(-) diff --git a/addons/terrain-editing/TerrainEditingControls+Editing.cs b/addons/terrain-editing/TerrainEditingControls+Editing.cs index fd4e29c..5873ceb 100644 --- a/addons/terrain-editing/TerrainEditingControls+Editing.cs +++ b/addons/terrain-editing/TerrainEditingControls+Editing.cs @@ -1,139 +1,222 @@ public partial class TerrainEditingControls { - bool _unhandledMotion = 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; + Terrain _currentTerrain = null; Material _editToolMaterial; public override void _EnterTree() { _editToolMaterial = new StandardMaterial3D { - VertexColorUseAsAlbedo = true, + AlbedoColor = Colors.Blue, ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, NoDepthTest = true, }; } + public override void _ExitTree() + => ClearEditToolMesh(); + public override void _Input(InputEvent ev) { - if (GetTerrain() is not Terrain terrain) return; + var viewport = !Engine.IsEditorHint() ? GetViewport() + : EditorInterface.Singleton.GetEditorViewport3D(); if (Engine.IsEditorHint()) { // Make sure to transform the input event to the 3D scene's viewport. - var viewport = EditorInterface.Singleton.GetEditorViewport3D(); var container = viewport.GetParent(); ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse()); if (ev is InputEventMouse m) m.GlobalPosition = m.Position; } - if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) { - _isSelecting = false; - GetViewport().SetInputAsHandled(); - return; - } - - 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 outerTile = terrain.GetTile(outerPos); - var innerHeight = terrain.GetTile(innerPos).Height[innerCorner]; - var outerHeight = outerTile.Height[outerCorner]; - - if (IsEqualApprox(outerHeight, innerHeight)) { - outerTile.Height[outerCorner] = innerHeight + amount; - terrain.SetTile(outerPos, outerTile); + if (ev is InputEventMouse mouse) { + if (viewport.GetVisibleRect().HasPoint(mouse.Position) + && (RayCastTerrain(mouse) is var (terrain, position))) { + var (tile, corner) = FindClosestTile(terrain, position); + + var drawSize = (ToolShape == ToolShape.Corner) ? 1 : DrawSize; + var isEven = (drawSize % 2) == 0; + var radius = FloorToInt(drawSize / 2.0f); + + // Offset tile position by corner. + if (isEven) tile = corner switch { + Corner.TopLeft => new(tile.X + 0, tile.Y + 0), + Corner.TopRight => new(tile.X + 1, tile.Y + 0), + Corner.BottomRight => new(tile.X + 1, tile.Y + 1), + Corner.BottomLeft => new(tile.X + 0, tile.Y + 1), + _ => throw new InvalidOperationException(), + }; + + IEnumerable GetTilesInSquare() { + var minX = tile.X - radius; + var minY = tile.Y - radius; + var maxX = tile.X + radius - (isEven ? 1 : 0); + var maxY = tile.Y + radius - (isEven ? 1 : 0); + for (var x = minX; x <= maxX; x++) + for (var y = minY; y <= maxY; y++) + yield return new(x, y); } - } - // Raise connected sides. - foreach (var side in Enum.GetValues()) { - foreach (var innerPos in selection.GetTilesFor(side)) { - var outerPos = innerPos.GetNeighbor(side); - - var innerTile = terrain.GetTile(innerPos); - var outerTile = terrain.GetTile(outerPos); - - var (innerCorner1, innerCorner2) = side.GetCorners(); - var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners(); - - var changed = false; - var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) }; - foreach (var (innerCorner, outerCorner) in matchingCorners) { - var innerHeight = innerTile.Height[innerCorner]; - var outerHeight = outerTile.Height[outerCorner]; - - if (IsEqualApprox(outerHeight, innerHeight)) { - outerTile.Height[outerCorner] = innerHeight + amount; - changed = true; - } - } - if (changed) terrain.SetTile(outerPos, outerTile); + IEnumerable GetTilesInRadius() { + var center = isEven + ? new Vector2(tile.X , tile.Y ) + : new Vector2(tile.X + 0.5f, tile.Y + 0.5f); + var distanceSqr = Pow(isEven ? radius - 0.25f : radius + 0.25f, 2); + return GetTilesInSquare() + .Where(tile => center.DistanceSquaredTo( + new Vector2(tile.X + 0.5f, tile.Y + 0.5f)) < distanceSqr); } - } - // Raise selected tiles themselves. - foreach (var pos in selection.GetAllTiles()) { - var tile = terrain.GetTile(pos); - tile.Height.Adjust(amount); - terrain.SetTile(pos, tile); + UpdateEditToolMesh(terrain, ToolShape switch { + // TODO: Edit corner, not full tile. + ToolShape.Corner => [tile], + ToolShape.Circle => GetTilesInRadius(), + ToolShape.Square => GetTilesInSquare(), + _ => throw new InvalidOperationException(), + }); + } else { + ClearEditToolMesh(); } - - terrain.UpdateMeshAndShape(); - terrain.NotifyPropertyListChanged(); - return; } - if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true }) - && (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null)) - { - GetViewport().SetInputAsHandled(); - if (!pressed2) return; + // if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) { + // _isSelecting = false; + // GetViewport().SetInputAsHandled(); + // return; + // } + + // 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 outerTile = terrain.GetTile(outerPos); + // var innerHeight = terrain.GetTile(innerPos).Height[innerCorner]; + // var outerHeight = outerTile.Height[outerCorner]; + + // if (IsEqualApprox(outerHeight, innerHeight)) { + // outerTile.Height[outerCorner] = innerHeight + amount; + // terrain.SetTile(outerPos, outerTile); + // } + // } + + // // Raise connected sides. + // foreach (var side in Enum.GetValues()) { + // foreach (var innerPos in selection.GetTilesFor(side)) { + // var outerPos = innerPos.GetNeighbor(side); + + // var innerTile = terrain.GetTile(innerPos); + // var outerTile = terrain.GetTile(outerPos); + + // var (innerCorner1, innerCorner2) = side.GetCorners(); + // var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners(); + + // var changed = false; + // var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) }; + // foreach (var (innerCorner, outerCorner) in matchingCorners) { + // var innerHeight = innerTile.Height[innerCorner]; + // var outerHeight = outerTile.Height[outerCorner]; + + // if (IsEqualApprox(outerHeight, innerHeight)) { + // outerTile.Height[outerCorner] = innerHeight + amount; + // changed = true; + // } + // } + // if (changed) terrain.SetTile(outerPos, outerTile); + // } + // } + + // // Raise selected tiles themselves. + // foreach (var pos in selection.GetAllTiles()) { + // var tile = terrain.GetTile(pos); + // tile.Height.Adjust(amount); + // terrain.SetTile(pos, tile); + // } + + // terrain.UpdateMeshAndShape(); + // terrain.NotifyPropertyListChanged(); + // return; + // } + + // if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true }) + // && (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null)) + // { + // GetViewport().SetInputAsHandled(); + // if (!pressed2) return; + + // var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1; + // var selection = TileRegion.From(_selection.Value); + + // foreach (var pos in selection.GetAllTiles()) { + // var tile = terrain.GetTile(pos); + // tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4); + // terrain.SetTile(pos, tile); + // } + + // terrain.UpdateMeshAndShape(); + // terrain.NotifyPropertyListChanged(); + // return; + // } + + // if (ev is InputEventMouseMotion) + // _unhandledMotion = true; + + // if ((ev is InputEventMouse mouse) && viewport.GetVisibleRect().HasPoint(mouse.Position)) + // OnInputRayCastTerrain(terrain, mouse); + } - var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1; - var selection = TileRegion.From(_selection.Value); + void UpdateEditToolMesh(Terrain terrain, IEnumerable tiles) + { + if (terrain != _currentTerrain) ClearEditToolMesh(); + _currentTerrain = terrain; - foreach (var pos in selection.GetAllTiles()) { - var tile = terrain.GetTile(pos); - tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4); - terrain.SetTile(pos, tile); - } + var mesh = terrain.GetOrCreateMesh("EditToolMesh"); + mesh.ClearSurfaces(); + mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); + + void AddLine(Vector3 start, Vector3 end) { + mesh.SurfaceAddVertex(start); + mesh.SurfaceAddVertex(end); + } - terrain.UpdateMeshAndShape(); - terrain.NotifyPropertyListChanged(); - return; + void AddQuad(Vector3 topLeft , Vector3 topRight, + Vector3 bottomRight, Vector3 bottomLeft) { + AddLine(topLeft , topRight ); + AddLine(topRight , bottomRight); + AddLine(bottomRight, bottomLeft ); + AddLine(bottomLeft , topLeft ); } - if (ev is InputEventMouseMotion) - _unhandledMotion = true; + foreach (var tile in tiles) { + var (topLeft, topRight, bottomRight, bottomLeft) + = terrain.GetTileCornerPositions(tile); + AddQuad(topLeft, topRight, bottomRight, bottomLeft); + } - if ((ev is InputEventMouse mouse) && (!Engine.IsEditorHint() || - EditorInterface.Singleton.GetEditorViewport3D() - .GetVisibleRect().HasPoint(mouse.Position))) - OnInputRayCastTerrain(terrain, mouse); + mesh.SurfaceEnd(); + mesh.SurfaceSetMaterial(0, _editToolMaterial); } - void OnInputRayCastTerrain(Terrain terrain, InputEventMouse ev) + void ClearEditToolMesh() + => _currentTerrain?.GetNodeOrNull("EditToolMesh")?.QueueFree(); + + (Terrain Terrain, Vector3 Position)? RayCastTerrain(InputEventMouse ev) { // Ray is cast from the editor camera's view. var camera = EditorInterface.Singleton.GetEditorViewport3D().GetCamera3D(); @@ -145,138 +228,31 @@ public partial class TerrainEditingControls var space = root.GetWorld3D().DirectSpaceState; var query = PhysicsRayQueryParameters3D.Create(from, to); - var result = space.IntersectRay(query); + var result = space.IntersectRay(query); var collider = (GodotObject)result.GetValueOrDefault("collider"); - if (collider == terrain) { - var pos = (Vector3)result["position"]; - // var normal = (Vector3)result["normal"]; - // var shape = (int)result["shape"]; - OnTerrainInput(terrain, ev, pos); - } + return (collider is Terrain terrain) + ? (terrain, (Vector3)result["position"]) + : null; } - void OnTerrainInput(Terrain terrain, InputEvent ev, Vector3 position) + static (TilePos, Corner) FindClosestTile(Terrain terrain, Vector3 position) { - if (terrain == null) return; - - var localPos = terrain.ToLocal(position); - var tilePos = terrain.ToTilePos(localPos); - - if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { - _selection = new(tilePos, tilePos); - _isSelecting = true; - GetViewport().SetInputAsHandled(); - return; - } - - if (ev is InputEventMouseMotion) { - _unhandledMotion = false; - _tileHover = tilePos; - if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos }; - } - } - - public override void _Process(double delta) - { - if (GetTerrain() is not Terrain terrain) { - _tileHover = null; - _selection = null; - _isSelecting = false; - terrain = null; - } - - if (_unhandledMotion) - _tileHover = null; - - if ((_tileHover != null) || (_selection != null)) { - var mesh = terrain.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 = terrain.GetTileCornerPositions(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 = terrain.GetTileCornerPositions(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(); - } - } - - public Terrain GetTerrain() => EditorInterface.Singleton.GetSelection() - .GetSelectedNodes().OfType().FirstOrDefault(); - - - 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)), - }; - } + var local = terrain.ToLocal(position); + + var tileX = local.X / terrain.TileSize + 0.5 + terrain.Size.X / 2; + var tileY = local.Z / terrain.TileSize + 0.5 + terrain.Size.Y / 2; + var tile = new TilePos(FloorToInt(tileX), FloorToInt(tileY)); + + var cornerX = RoundToInt(PosMod(tileX, 1)); + var cornerY = RoundToInt(PosMod(tileY, 1)); + var corner = (cornerX, cornerY) switch { + (0, 0) => Corner.TopLeft, + (1, 0) => Corner.TopRight, + (1, 1) => Corner.BottomRight, + (0, 1) => Corner.BottomLeft, + _ => throw new InvalidOperationException(), + }; - public IEnumerable GetAllTiles() - { - for (var x = Left; x <= Right; x++) - for (var y = Top; y <= Bottom; y++) - yield return new(x, y); - } + return (tile, corner); } } diff --git a/addons/terrain-editing/TerrainEditingControls.tscn b/addons/terrain-editing/TerrainEditingControls.tscn index 9b71509..54bdad2 100644 --- a/addons/terrain-editing/TerrainEditingControls.tscn +++ b/addons/terrain-editing/TerrainEditingControls.tscn @@ -78,7 +78,7 @@ horizontal_alignment = 1 custom_minimum_size = Vector2(0, 80) layout_mode = 2 size_flags_horizontal = 1 -min_value = -16.0 +min_value = -8.0 max_value = -1.0 value = -1.0 @@ -87,6 +87,12 @@ layout_mode = 2 [node name="Grass" type="Button" parent="."] layout_mode = 2 +theme_override_colors/icon_normal_color = Color(1, 1, 1, 1) +theme_override_colors/icon_focus_color = Color(1, 1, 1, 1) +theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961) theme_override_constants/icon_max_width = 16 disabled = true toggle_mode = true @@ -94,6 +100,12 @@ icon = SubResource("ImageTexture_btyvd") [node name="Dirt" type="Button" parent="."] layout_mode = 2 +theme_override_colors/icon_normal_color = Color(1, 1, 1, 1) +theme_override_colors/icon_focus_color = Color(1, 1, 1, 1) +theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961) theme_override_constants/icon_max_width = 16 disabled = true toggle_mode = true @@ -103,6 +115,12 @@ flat = true [node name="Rock" type="Button" parent="."] layout_mode = 2 +theme_override_colors/icon_normal_color = Color(1, 1, 1, 1) +theme_override_colors/icon_focus_color = Color(1, 1, 1, 1) +theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961) theme_override_constants/icon_max_width = 16 disabled = true toggle_mode = true @@ -112,6 +130,12 @@ flat = true [node name="Sand" type="Button" parent="."] layout_mode = 2 +theme_override_colors/icon_normal_color = Color(1, 1, 1, 1) +theme_override_colors/icon_focus_color = Color(1, 1, 1, 1) +theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_color = Color(1, 1, 1, 1) +theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1) +theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961) theme_override_constants/icon_max_width = 16 disabled = true toggle_mode = true diff --git a/terrain/Terrain.cs b/terrain/Terrain.cs index 4ed38c4..8742ddf 100644 --- a/terrain/Terrain.cs +++ b/terrain/Terrain.cs @@ -33,11 +33,6 @@ public partial class Terrain => (pos.X >= 0) && (pos.X < Size.X) && (pos.Y >= 0) && (pos.Y < Size.Y); - /// Transforms a 3D position local to the equivalent tile position. - public TilePos ToTilePos(Vector3 localPos) - => new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f), - RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f)); - public void UpdateMeshAndShape() { diff --git a/terrain/Tile.cs b/terrain/Tile.cs index 1872a6d..994a081 100644 --- a/terrain/Tile.cs +++ b/terrain/Tile.cs @@ -106,6 +106,16 @@ public struct Corners(T topLeft, T topRight, T bottomRight, T bottomLeft) default: throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)); } } } + + public readonly void Deconstruct( + out T topLeft , out T topRight, + out T bottomRight, out T bottomLeft) + { + topLeft = TopLeft; + topRight = TopRight; + bottomRight = BottomRight; + bottomLeft = BottomLeft; + } } public static class CornersExtensions