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.
246 lines
8.4 KiB
246 lines
8.4 KiB
3 weeks ago
|
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<TilePos> 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<TilePos> 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<Corner>())
|
||
|
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<Corner>()) {
|
||
|
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<float>)>();
|
||
|
var tilesChanged = new List<(TilePos, Corners<float>)>();
|
||
|
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<TilePos> 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<Corner, (int X, int Y, Corner Opposite)[]> _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<float> 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<float> 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<float>(
|
||
|
reader.ReadSingle(), reader.ReadSingle(),
|
||
|
reader.ReadSingle(), reader.ReadSingle());
|
||
|
yield return (new(x, y), corners);
|
||
|
}
|
||
|
}
|
||
|
}
|