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