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