// 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); } }