// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph.Actors;
using FlaxEditor.Tools.Terrain.Brushes;
using FlaxEditor.Tools.Terrain.Paint;
using FlaxEditor.Tools.Terrain.Undo;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
///
/// Terrain painting tool mode.
///
///
[HideInEditor]
public class PaintTerrainGizmoMode : EditorGizmoMode
{
private struct SplatmapData
{
public IntPtr DataPtr;
public int Size;
public void EnsureCapacity(int size)
{
if (Size < size)
{
if (DataPtr != IntPtr.Zero)
Marshal.FreeHGlobal(DataPtr);
DataPtr = Marshal.AllocHGlobal(size);
Utils.MemoryClear(DataPtr, (ulong)size);
Size = size;
}
}
public void Free()
{
if (DataPtr == IntPtr.Zero)
return;
Marshal.FreeHGlobal(DataPtr);
DataPtr = IntPtr.Zero;
Size = 0;
}
}
private EditTerrainMapAction _activeAction;
private SplatmapData[] _cachedSplatmapData = new SplatmapData[2];
///
/// The terrain painting gizmo.
///
public PaintTerrainGizmo Gizmo;
///
/// The tool modes.
///
public enum ModeTypes
{
///
/// The single layer mode.
///
SingleLayer,
}
///
/// The brush types.
///
public enum BrushTypes
{
///
/// The circle brush.
///
CircleBrush,
}
private readonly Mode[] _modes =
{
new SingleLayerMode(),
};
private readonly Brush[] _brushes =
{
new CircleBrush(),
};
private ModeTypes _modeType = ModeTypes.SingleLayer;
private BrushTypes _brushType = BrushTypes.CircleBrush;
///
/// Occurs when tool mode gets changed.
///
public event Action ToolModeChanged;
///
/// Gets the current tool mode (enum).
///
public ModeTypes ToolModeType
{
get => _modeType;
set
{
if (_modeType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change paint tool mode during terrain editing.");
_modeType = value;
ToolModeChanged?.Invoke();
}
}
}
///
/// Gets the current tool mode.
///
public Mode CurrentMode => _modes[(int)_modeType];
///
/// Gets the single layer mode instance.
///
public SingleLayerMode SingleLayerMode => _modes[(int)ModeTypes.SingleLayer] as SingleLayerMode;
///
/// Occurs when tool brush gets changed.
///
public event Action ToolBrushChanged;
///
/// Gets the current tool brush (enum).
///
public BrushTypes ToolBrushType
{
get => _brushType;
set
{
if (_brushType != value)
{
if (_activeAction != null)
throw new InvalidOperationException("Cannot change sculpt tool brush type during terrain editing.");
_brushType = value;
ToolBrushChanged?.Invoke();
}
}
}
///
/// Gets the current brush.
///
public Brush CurrentBrush => _brushes[(int)_brushType];
///
/// Gets the circle brush instance.
///
public CircleBrush CircleBrush => _brushes[(int)BrushTypes.CircleBrush] as CircleBrush;
///
/// The last valid cursor position of the brush (in world space).
///
public Vector3 CursorPosition { get; private set; }
///
/// Flag used to indicate whenever last cursor position of the brush is valid.
///
public bool HasValidHit { get; private set; }
///
/// Describes the terrain patch link.
///
public struct PatchLocation
{
///
/// The patch coordinates.
///
public Int2 PatchCoord;
}
///
/// The selected terrain patches collection that are under cursor (affected by the brush).
///
public readonly List PatchesUnderCursor = new List();
///
/// Describes the terrain chunk link.
///
public struct ChunkLocation
{
///
/// The patch coordinates.
///
public Int2 PatchCoord;
///
/// The chunk coordinates.
///
public Int2 ChunkCoord;
}
///
/// The selected terrain chunk collection that are under cursor (affected by the brush).
///
public readonly List ChunksUnderCursor = new List();
///
/// Gets the selected terrain actor (see ).
///
public FlaxEngine.Terrain SelectedTerrain
{
get
{
var sceneEditing = Editor.Instance.SceneEditing;
var terrainNode = sceneEditing.SelectionCount == 1 ? sceneEditing.Selection[0] as TerrainNode : null;
return (FlaxEngine.Terrain)terrainNode?.Actor;
}
}
///
/// Gets the world bounds of the brush located at the current cursor position (defined by ). Valid only if is set to true.
///
public BoundingBox CursorBrushBounds
{
get
{
const float brushExtentY = 10000.0f;
float brushSizeHalf = CurrentBrush.Size * 0.5f;
Vector3 center = CursorPosition;
BoundingBox box;
box.Minimum = new Vector3(center.X - brushSizeHalf, center.Y - brushSizeHalf - brushExtentY, center.Z - brushSizeHalf);
box.Maximum = new Vector3(center.X + brushSizeHalf, center.Y + brushSizeHalf + brushExtentY, center.Z + brushSizeHalf);
return box;
}
}
///
/// Gets the splatmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
///
/// The minimum buffer size (in bytes).
/// The splatmap index for which to return/create the temp buffer.
/// The allocated memory using interface.
public IntPtr GetSplatmapTempBuffer(int size, int splatmapIndex)
{
ref var splatmapData = ref _cachedSplatmapData[splatmapIndex];
splatmapData.EnsureCapacity(size);
return splatmapData.DataPtr;
}
///
/// Gets the current edit terrain undo system action. Use it to record the data for the undo restoring after terrain editing.
///
internal EditTerrainMapAction CurrentEditUndoAction => _activeAction;
///
public override void Init(IGizmoOwner owner)
{
base.Init(owner);
Gizmo = new PaintTerrainGizmo(owner, this);
Gizmo.PaintStarted += OnPaintStarted;
Gizmo.PaintEnded += OnPaintEnded;
}
///
public override void OnActivated()
{
base.OnActivated();
Owner.Gizmos.Active = Gizmo;
ClearCursor();
}
///
public override void OnDeactivated()
{
base.OnDeactivated();
// Free temporary memory buffer
foreach (ref var splatmapData in _cachedSplatmapData.AsSpan())
splatmapData.Free();
}
///
/// Clears the cursor location information cached within the gizmo mode.
///
public void ClearCursor()
{
HasValidHit = false;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
}
///
/// Sets the cursor location in the world space. Updates the brush location and cached affected chunks.
///
/// The cursor hit location on the selected terrain.
public void SetCursor(ref Vector3 hitPosition)
{
HasValidHit = true;
CursorPosition = hitPosition;
PatchesUnderCursor.Clear();
ChunksUnderCursor.Clear();
// Find patches and chunks affected by the brush
var terrain = SelectedTerrain;
if (terrain == null)
throw new InvalidOperationException("Cannot set cursor then no terrain is selected.");
var brushBounds = CursorBrushBounds;
var patchesCount = terrain.PatchesCount;
for (int patchIndex = 0; patchIndex < patchesCount; patchIndex++)
{
terrain.GetPatchBounds(patchIndex, out BoundingBox tmp);
if (!tmp.Intersects(ref brushBounds))
continue;
terrain.GetPatchCoord(patchIndex, out var patchCoord);
PatchesUnderCursor.Add(new PatchLocation() { PatchCoord = patchCoord });
for (int chunkIndex = 0; chunkIndex < FlaxEngine.Terrain.PatchChunksCount; chunkIndex++)
{
terrain.GetChunkBounds(patchIndex, chunkIndex, out tmp);
if (!tmp.Intersects(ref brushBounds))
continue;
var chunkCoord = new Int2(chunkIndex % FlaxEngine.Terrain.PatchEdgeChunksCount, chunkIndex / FlaxEngine.Terrain.PatchEdgeChunksCount);
ChunksUnderCursor.Add(new ChunkLocation() { PatchCoord = patchCoord, ChunkCoord = chunkCoord });
}
}
}
private void OnPaintStarted()
{
if (_activeAction != null)
throw new InvalidOperationException("Terrain paint start/end resynchronization.");
var terrain = SelectedTerrain;
_activeAction = new EditTerrainSplatMapAction(terrain);
}
private void OnPaintEnded()
{
if (_activeAction != null)
{
if (_activeAction.HasAnyModification)
{
_activeAction.OnEditingEnd();
Editor.Instance.Undo.AddAction(_activeAction);
}
_activeAction = null;
}
}
}
}