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.
266 lines
9.9 KiB
266 lines
9.9 KiB
using System.IO; |
|
using System.Runtime.InteropServices; |
|
|
|
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; } |
|
|
|
// Dummy value to satisfy the overly careful compiler. |
|
static bool _dummy = false; |
|
|
|
Material _editToolMaterial; |
|
public override void _EnterTree() |
|
{ |
|
_editToolMaterial = new StandardMaterial3D { |
|
VertexColorUseAsAlbedo = true, |
|
BlendMode = BaseMaterial3D.BlendModeEnum.Mix, |
|
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, |
|
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, |
|
NoDepthTest = true, |
|
}; |
|
} |
|
|
|
Vector3 _lastPosition; |
|
public bool EditorInput(InputEvent ev, Control controls, Vector3 maybePos) |
|
{ |
|
var prevent_default = false; |
|
|
|
// If 'maybePosition' is Vector3.Zero, use previous know position, otherwise update. |
|
var position = (maybePos == Vector3.Zero) ? _lastPosition : (_lastPosition = maybePos); |
|
|
|
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); |
|
} |
|
|
|
// TODO: Handle different tool modes, such as painting. |
|
// 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. |
|
|
|
// Holds onto all the tiles and which of their corners corners will be affected by this edit operation. |
|
var tilesToChange = new Dictionary<TilePos, Corners<bool>>(); |
|
ref Corners<bool> GetTileToChange(TilePos position) |
|
// Don't look at this black magic. The Dictionary type should have this by default I swear! |
|
=> ref CollectionsMarshal.GetValueRefOrAddDefault(tilesToChange, position, out _dummy); |
|
|
|
if (toolShape == ToolShape.Corner) { |
|
// Modify selected corner itself. |
|
GetTileToChange(hover.Position)[hover.Corner] = true; |
|
|
|
if (isConnected) { |
|
var height = GetTile(hover.Position).Height[hover.Corner]; |
|
foreach (var neighbor in GetNeighbors(hover.Position, hover.Corner)) { |
|
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; |
|
if (neighborHeight != height) continue; |
|
GetTileToChange(neighbor.Position)[neighbor.Corner] = true; |
|
} |
|
} |
|
} else { |
|
var tiles = (toolShape switch { |
|
ToolShape.Circle => GetTilesInRadius(), |
|
ToolShape.Square => GetTilesInSquare(), |
|
_ => throw new InvalidOperationException(), |
|
}).ToHashSet(); |
|
|
|
// Modify selected tiles themselves. |
|
foreach (var pos in tiles) |
|
GetTileToChange(pos) = new(true); |
|
|
|
// 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. |
|
if (isConnected) foreach (var pos in tiles) { |
|
var tile = GetTile(pos); |
|
foreach (var corner in Enum.GetValues<Corner>()) { |
|
var height = tile.Height[corner]; |
|
foreach (var neighbor in GetNeighbors(pos, corner)) { |
|
if (tiles.Contains(neighbor.Position)) continue; |
|
var neighborHeight = GetTile(neighbor.Position).Height[neighbor.Corner]; |
|
if (neighborHeight != height) continue; |
|
GetTileToChange(neighbor.Position)[neighbor.Corner] = true; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Raise / lower the terrain when left mouse button is pressed. |
|
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { |
|
prevent_default = true; |
|
|
|
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 (pos, corners) in tilesToChange) { |
|
var tile = GetTile(pos); |
|
tilesPrevious.Add((pos, tile.Height)); |
|
|
|
var newHeight = tile.Height; |
|
if (corners.TopLeft ) newHeight.TopLeft = isFlatten ? amount : newHeight.TopLeft + amount; |
|
if (corners.TopRight ) newHeight.TopRight = isFlatten ? amount : newHeight.TopRight + amount; |
|
if (corners.BottomRight) newHeight.BottomRight = isFlatten ? amount : newHeight.BottomRight + amount; |
|
if (corners.BottomLeft ) newHeight.BottomLeft = isFlatten ? amount : newHeight.BottomLeft + 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(tilesToChange); |
|
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(Dictionary<TilePos, Corners<bool>> tiles) |
|
{ |
|
var mesh = GetOrCreateMesh("EditToolMesh"); |
|
mesh.ClearSurfaces(); |
|
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); |
|
|
|
void AddLine((Vector3 Position, bool Visible) start, |
|
(Vector3 Position, bool Visible) end) { |
|
mesh.SurfaceSetColor(start.Visible ? Colors.Blue : Colors.Transparent); |
|
mesh.SurfaceAddVertex(start.Position); |
|
mesh.SurfaceSetColor(end.Visible ? Colors.Blue : Colors.Transparent); |
|
mesh.SurfaceAddVertex(end.Position); |
|
} |
|
|
|
foreach (var (tile, visible) in tiles) { |
|
var positions = GetTileCornerPositions(tile); |
|
foreach (var side in Enum.GetValues<Side>()) { |
|
var (corner1, corner2) = side.GetCorners(); |
|
if (!visible[corner1] && !visible[corner2]) continue; |
|
AddLine((positions[corner1], visible[corner1]), |
|
(positions[corner2], visible[corner2])); |
|
} |
|
} |
|
|
|
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 Position, Corner 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); |
|
} |
|
} |
|
}
|
|
|