// Copyright (c) 2012-2023 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.Sculpt;
using FlaxEditor.Tools.Terrain.Undo;
using FlaxEditor.Viewport;
using FlaxEditor.Viewport.Modes;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain
{
///
/// Terrain carving tool mode.
///
///
[HideInEditor]
public class SculptTerrainGizmoMode : EditorGizmoMode
{
private IntPtr _cachedHeightmapData;
private int _cachedHeightmapDataSize;
private EditTerrainMapAction _activeAction;
///
/// The terrain carving gizmo.
///
public SculptTerrainGizmo Gizmo;
///
/// The tool modes.
///
public enum ModeTypes
{
///
/// The sculpt mode.
///
[Tooltip("Sculpt tool mode. Edits terrain heightmap by moving area affected by brush up or down.")]
Sculpt,
///
/// The smooth mode.
///
[Tooltip("Sculpt tool mode that smooths the terrain heightmap area affected by brush.")]
Smooth,
///
/// The flatten mode.
///
[Tooltip("Sculpt tool mode that flattens the terrain heightmap area affected by brush to the target value.")]
Flatten,
///
/// The noise mode.
///
[Tooltip("Sculpt tool mode that applies the noise to the terrain heightmap area affected by brush.")]
Noise,
///
/// The holes mode.
///
[Tooltip("Terrain holes creating tool mode edits terrain holes mask by changing area affected by brush.")]
Holes,
}
///
/// The brush types.
///
public enum BrushTypes
{
///
/// The circle brush.
///
CircleBrush,
}
private readonly Mode[] _modes =
{
new SculptMode(),
new SmoothMode(),
new FlattenMode(),
new NoiseMode(),
new HolesMode(),
};
private readonly Brush[] _brushes =
{
new CircleBrush(),
};
private ModeTypes _modeType = ModeTypes.Sculpt;
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 sculpt tool mode during terrain editing.");
_modeType = value;
ToolModeChanged?.Invoke();
}
}
}
///
/// Gets the current tool mode.
///
public Mode CurrentMode => _modes[(int)_modeType];
///
/// Gets the sculpt mode instance.
///
public SculptMode SculptMode => _modes[(int)ModeTypes.Sculpt] as SculptMode;
///
/// Gets the smooth mode instance.
///
public SmoothMode SmoothMode => _modes[(int)ModeTypes.Smooth] as SmoothMode;
///
/// 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 heightmap temporary scratch memory buffer used to modify terrain samples. Allocated memory is unmanaged by GC.
///
/// The minimum buffer size (in bytes).
/// The allocated memory using interface.
public IntPtr GetHeightmapTempBuffer(int size)
{
if (_cachedHeightmapDataSize < size)
{
if (_cachedHeightmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedHeightmapData);
}
_cachedHeightmapData = Marshal.AllocHGlobal(size);
_cachedHeightmapDataSize = size;
}
return _cachedHeightmapData;
}
///
/// 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 SculptTerrainGizmo(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
if (_cachedHeightmapData != IntPtr.Zero)
{
Marshal.FreeHGlobal(_cachedHeightmapData);
_cachedHeightmapData = IntPtr.Zero;
_cachedHeightmapDataSize = 0;
}
}
///
/// 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;
if (CurrentMode.EditHoles)
_activeAction = new EditTerrainHolesMapAction(terrain);
else
_activeAction = new EditTerrainHeightMapAction(terrain);
}
private void OnPaintEnded()
{
if (_activeAction != null)
{
if (_activeAction.HasAnyModification)
{
_activeAction.OnEditingEnd();
Editor.Instance.Undo.AddAction(_activeAction);
}
_activeAction = null;
}
}
}
}