// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using FlaxEngine;
namespace FlaxEditor.Tools.Terrain.Undo
{
///
/// The terrain heightmap or visibility map editing action that records before and after states to swap between unmodified and modified terrain data.
///
///
[Serializable]
abstract class EditTerrainMapAction : IUndoAction
{
///
/// The compact data for the terrain patch modification.
///
[Serializable]
protected struct PatchData
{
///
/// The patch coordinates.
///
public Int2 PatchCoord;
///
/// The data before (allocated on heap memory or null).
///
public IntPtr Before;
///
/// The data after (allocated on heap memory or null).
///
public IntPtr After;
///
/// The custom tag.
///
public object Tag;
}
[Serialize]
protected readonly Guid _terrain;
[Serialize]
protected readonly int _heightmapLength;
[Serialize]
protected readonly int _heightmapDataSize;
[Serialize]
protected readonly List _patches;
[Serialize]
protected readonly List _navmeshBoundsModifications;
[Serialize]
protected readonly float _dirtyNavMeshTimeoutMs;
[NoSerialize]
public bool HasAnyModification => _patches.Count > 0;
///
/// Gets the terrain.
///
[NoSerialize]
public FlaxEngine.Terrain Terrain
{
get
{
var terrainId = _terrain;
return FlaxEngine.Object.Find(ref terrainId);
}
}
///
/// Initializes a new instance of the class.
///
/// Use to mark new patches to record and to finalize patches data after editing action.
/// The terrain.
/// The data stride (eg. sizeof(float)).
protected EditTerrainMapAction(FlaxEngine.Terrain terrain, int stride)
{
_terrain = terrain.ID;
_patches = new List(4);
var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
_heightmapLength = heightmapSize * heightmapSize;
_heightmapDataSize = _heightmapLength * stride;
// Auto NavMesh rebuild
var editorOptions = Editor.Instance.Options.Options;
bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode;
if (!isPlayMode && editorOptions.General.AutoRebuildNavMesh)
{
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
{
_navmeshBoundsModifications = new List();
_dirtyNavMeshTimeoutMs = editorOptions.General.AutoRebuildNavMeshTimeoutMs;
}
}
}
///
/// Adds modified bounds to the undo actor modifications list.
///
/// The world-space bounds.
public void AddDirtyBounds(ref BoundingBox bounds)
{
_navmeshBoundsModifications?.Add(bounds);
}
///
/// Checks if the patch at the given coordinates has been already added.
///
/// The patch coordinates.
/// True if patch has been added, otherwise false.
public bool HashPatch(ref Int2 patchCoord)
{
for (int i = 0; i < _patches.Count; i++)
{
if (_patches[i].PatchCoord == patchCoord)
return true;
}
return false;
}
///
/// Adds the patch to the action and records its current state.
///
/// The patch coordinates.
/// The custom argument (per patch).
public void AddPatch(ref Int2 patchCoord, object tag = null)
{
var data = Marshal.AllocHGlobal(_heightmapDataSize);
var source = GetData(ref patchCoord, tag);
Utils.MemoryCopy(data, source, (ulong)_heightmapDataSize);
_patches.Add(new PatchData
{
PatchCoord = patchCoord,
Before = data,
After = IntPtr.Zero,
Tag = tag,
});
}
///
/// Called when terrain action editing ends. Record the `after` state of the patches.
///
public void OnEditingEnd()
{
if (_patches.Count == 0)
return;
for (int i = 0; i < _patches.Count; i++)
{
var patch = _patches[i];
if (patch.After != IntPtr.Zero)
throw new InvalidOperationException("Invalid terrain edit undo action usage.");
var data = Marshal.AllocHGlobal(_heightmapDataSize);
var source = GetData(ref patch.PatchCoord, patch.Tag);
Utils.MemoryCopy(data, source, (ulong)_heightmapDataSize);
patch.After = data;
_patches[i] = patch;
}
// Update navmesh
var scene = Terrain.Scene;
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(scene);
}
///
public abstract string ActionString { get; }
///
public void Do()
{
Set(x => x.After);
}
///
public void Undo()
{
Set(x => x.Before);
}
///
public void Dispose()
{
// Ensure to release memory
for (int i = 0; i < _patches.Count; i++)
{
Marshal.FreeHGlobal(_patches[i].Before);
Marshal.FreeHGlobal(_patches[i].After);
}
_patches.Clear();
_navmeshBoundsModifications?.Clear();
}
private void Set(Func dataGetter)
{
// Update patches
for (int i = 0; i < _patches.Count; i++)
{
var patch = _patches[i];
var data = dataGetter(patch);
SetData(ref patch.PatchCoord, data, patch.Tag);
}
// Update navmesh
var scene = Terrain.Scene;
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);
}
///
/// Gets the patch data.
///
/// The patch coordinates.
/// The custom argument (per patch).
/// The data buffer (pointer to unmanaged memory).
protected abstract IntPtr GetData(ref Int2 patchCoord, object tag);
///
/// Sets the patch data.
///
/// The patch coordinates.
/// The patch data.
/// The custom argument (per patch).
protected abstract void SetData(ref Int2 patchCoord, IntPtr data, object tag);
}
}