Editor/LineEdit.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace TerrainLines;
internal sealed class LineEdit : IDisposable
{
internal readonly Terrain Terrain;
private readonly TerrainStorage _storage;
private readonly ushort[] _heightMap;
private readonly uint[] _controlMap;
private readonly Transform _transform;
private readonly int _resolution;
private readonly float _size, _height;
private List<LineTile> _tiles = [];
private readonly Dictionary<(int, int), LineTile> _previewTiles = new();
private readonly Func<int, int, ushort> _readPreview;
private bool _disposed;
internal bool HasChanges => _tiles.Count > 0;
internal bool IsCurrent => !_disposed && Terrain.IsValid() && ReferenceEquals( Terrain.Storage, _storage )
&& ReferenceEquals( _storage.HeightMap, _heightMap ) && ReferenceEquals( _storage.ControlMap, _controlMap )
&& _storage.Resolution == _resolution && _storage.TerrainSize == _size && _storage.TerrainHeight == _height
&& Terrain.WorldTransform == _transform;
internal LineEdit( Terrain terrain )
{
Terrain = terrain;
_readPreview = ReadPreview;
_storage = terrain.Storage;
_heightMap = _storage.HeightMap;
_controlMap = _storage.ControlMap;
_resolution = _storage.Resolution;
_size = _storage.TerrainSize;
_height = _storage.TerrainHeight;
_transform = terrain.WorldTransform;
terrain.OnTerrainModified += TerrainModified;
}
internal void Preview( IReadOnlyList<Vector3> points, LineSettings settings )
{
if ( !IsCurrent ) throw new InvalidOperationException( "The terrain changed. Start a new line." );
RestorePreview();
_tiles = LineRaster.Build( _storage, points, settings, Terrain.WorldScale.x );
_previewTiles.Clear();
foreach ( var tile in _tiles )
{
_previewTiles.Add( (tile.X / LineRaster.TileSize, tile.Y / LineRaster.TileSize), tile );
tile.UploadPreview( Terrain );
}
}
internal float SampleHeight( float x, float y ) => LineRaster.SampleHeight( _storage, x, y, _readPreview );
private ushort ReadPreview( int x, int y )
{
if ( !_disposed && _previewTiles.TryGetValue( (x / LineRaster.TileSize, y / LineRaster.TileSize), out var tile ) && tile.AfterHeight is not null )
return tile.AfterHeight[(y - tile.Y) * tile.Width + x - tile.X];
return _storage.HeightMap[y * _resolution + x];
}
internal bool Apply( SceneEditorSession session )
{
if ( !IsCurrent || !ReferenceEquals( session?.Scene, Terrain.Scene ) )
throw new InvalidOperationException( "The selected terrain or scene changed. Start a new line." );
if ( _tiles.Any( tile => !tile.Matches( _storage ) ) )
throw new InvalidOperationException( "Terrain data changed during the preview. Cancel and draw again." );
if ( !HasChanges ) { Dispose(); return false; }
var delta = new LineDelta( Terrain, _tiles );
// Detach before native synchronization raises OnTerrainModified.
Terrain.OnTerrainModified -= TerrainModified;
_disposed = true;
_tiles = [];
_previewTiles.Clear();
delta.Write( true );
session.AddUndo( "Terrain Lines", () => delta.Write( false ), () => delta.Write( true ) );
session.HasUnsavedChanges = true;
return true;
}
private void TerrainModified( Terrain.SyncFlags flags, RectInt region ) => Dispose();
private void RestorePreview()
{
if ( !Terrain.IsValid() || !ReferenceEquals( Terrain.Storage, _storage ) || _storage.Resolution != _resolution ) return;
foreach ( var tile in _tiles ) tile.RestorePreview( Terrain );
}
public void Dispose()
{
if ( _disposed ) return;
if ( Terrain.IsValid() ) Terrain.OnTerrainModified -= TerrainModified;
RestorePreview();
_tiles.Clear();
_previewTiles.Clear();
_disposed = true;
}
}
internal sealed class LineDelta
{
private readonly Terrain _terrain;
private readonly TerrainStorage _storage;
private readonly int _resolution;
private readonly List<LineTile> _tiles;
private readonly RectInt _region;
private readonly Terrain.SyncFlags _flags;
internal LineDelta( Terrain terrain, List<LineTile> tiles )
{
_terrain = terrain;
_storage = terrain.Storage;
_resolution = _storage.Resolution;
_tiles = tiles;
int x = tiles.Min( t => t.X ), y = tiles.Min( t => t.Y );
_region = new RectInt( x, y, tiles.Max( t => t.X + t.Width ) - x, tiles.Max( t => t.Y + t.Height ) - y );
foreach ( var tile in tiles ) _flags |= tile.Flags;
}
internal void Write( bool after )
{
if ( !_terrain.IsValid() || !ReferenceEquals( _terrain.Storage, _storage ) || _storage.Resolution != _resolution ) return;
foreach ( var tile in _tiles ) tile.WriteStorage( _storage, after );
// One native synchronization updates collision, normals, storage and terrain-dependent systems.
_terrain.ApplyStorageChanges( _flags, _region );
}
}