|
|
|
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;
|
|
|
|
public override void _EnterTree()
|
|
|
|
{
|
|
|
|
_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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|