You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
282 lines
9.4 KiB
282 lines
9.4 KiB
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; |
|
|
|
Material _editToolMaterial; |
|
void OnEditingReady() |
|
{ |
|
_editToolMaterial = new StandardMaterial3D { |
|
VertexColorUseAsAlbedo = true, |
|
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, |
|
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, |
|
NoDepthTest = true, |
|
}; |
|
} |
|
|
|
public override void _Input(InputEvent ev) |
|
{ |
|
if (GetTerrain() is not Terrain terrain) return; |
|
|
|
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<SubViewportContainer>(); |
|
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<Corner>()) { |
|
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<Side>()) { |
|
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) && (!Engine.IsEditorHint() || |
|
EditorInterface.Singleton.GetEditorViewport3D() |
|
.GetVisibleRect().HasPoint(mouse.Position))) |
|
OnInputRayCastTerrain(terrain, mouse); |
|
} |
|
|
|
void OnInputRayCastTerrain(Terrain terrain, InputEventMouse ev) |
|
{ |
|
// Ray is cast from the editor camera's view. |
|
var camera = EditorInterface.Singleton.GetEditorViewport3D().GetCamera3D(); |
|
var from = camera.ProjectRayOrigin(ev.Position); |
|
var to = from + camera.ProjectRayNormal(ev.Position) * camera.Far; |
|
|
|
// Actual collision is done in the edited scene though. |
|
var root = (Node3D)EditorInterface.Singleton.GetEditedSceneRoot(); |
|
var space = root.GetWorld3D().DirectSpaceState; |
|
var query = PhysicsRayQueryParameters3D.Create(from, to); |
|
|
|
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); |
|
} |
|
} |
|
|
|
void OnTerrainInput(Terrain terrain, InputEvent ev, 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<Terrain>().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<TilePos> 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<TilePos> GetAllTiles() |
|
{ |
|
for (var x = Left; x <= Right; x++) |
|
for (var y = Top; y <= Bottom; y++) |
|
yield return new(x, y); |
|
} |
|
} |
|
}
|
|
|