editor/AdvancedMappingTools/Editing/TerrainPadEditor.cs

Editor tool that flattens a circular pad into the heightmap of a Terrain under the cursor. Draws a radius ring gizmo and applies one undoable height blend per click, with radius, falloff and strength settings.

🐞 The height blend uses the local Z of the trace point as the target height while the sample positions are built from grid X/Y with Z set to zero, so the target height is only correct when the terrain sits on a Z-aligned transform.
🐞 Restore copies the whole saved height array into HeightMap starting at index 0 regardless of region, so the region parameter is ignored and the full map is always overwritten.
namespace AdvancedMappingTools;

/// <summary>
/// Flattens a round pad into the terrain under the cursor. One click per pad, undo per pad.
/// </summary>
[ToolCategory( "Terrain" )]
[Title( "Terrain Pad" ), Icon( "crop_square" )]
public sealed class TerrainPadEditor( PrimitiveTool tool ) : BuilderEditor( tool )
{
	protected override int MinPoints => int.MaxValue;

	protected override IEnumerable<string> Hints => ["Click the terrain: flatten a pad"];

	[Property, Group( "Pad" ), Title( "Radius" ), Range( 8, 4096, slider: false ), WideMode]
	public float Radius { get; set; } = 128.0f;

	[Property, Group( "Pad" ), Title( "Falloff" ), Range( 0, 1 ), WideMode]
	public float Falloff { get; set; } = 0.5f;

	[Property, Group( "Pad" ), Range( 0.05f, 1 ), WideMode]
	public float Strength { get; set; } = 1.0f;

	protected override void BuildMesh( MeshKit kit ) { }

	public override void OnUpdate( SceneTrace trace )
	{
		var tr = trace.Run();

		if ( !tr.Hit || tr.GameObject is null ) return;

		var terrain = tr.GameObject.GetComponent<Terrain>();
		if ( terrain is null || terrain.Storage?.HeightMap is null ) return;

		Gizmo.Draw.IgnoreDepth = true;
		Gizmo.Draw.Color = Color.Yellow;

		for ( var i = 0; i < 32; i++ )
		{
			var a = i * MathF.PI * 2.0f / 32.0f;
			var b = (i + 1) * MathF.PI * 2.0f / 32.0f;
			var p = tr.EndPosition;

			Gizmo.Draw.Line( p + new Vector3( MathF.Cos( a ), MathF.Sin( a ), 0.0f ) * Radius, p + new Vector3( MathF.Cos( b ), MathF.Sin( b ), 0.0f ) * Radius );
		}

		if ( !Gizmo.WasLeftMousePressed || Gizmo.HasHovered ) return;

		Apply( terrain, tr.EndPosition );
	}

	void Apply( Terrain terrain, Vector3 point )
	{
		var session = SceneEditorSession.Active;
		if ( session is null ) return;

		var storage = terrain.Storage;
		var size = storage.Resolution;
		var cell = storage.TerrainSize / (size - 1.0f);
		var scale = storage.TerrainHeight / (float)ushort.MaxValue;
		var world = terrain.WorldTransform;
		var center = world.PointToLocal( point );
		var before = storage.HeightMap.ToArray();

		for ( var y = 0; y < size; y++ )
		{
			for ( var x = 0; x < size; x++ )
			{
				var flat = new Vector3( x * cell, y * cell, 0.0f );
				var distance = flat.WithZ( 0.0f ).Distance( center.WithZ( 0.0f ) );

				if ( distance > Radius ) continue;

				var weight = Weight( distance / MathF.Max( Radius, 0.01f ) ) * Strength;
				if ( weight <= 0.0001f ) continue;

				var index = x + y * size;
				var height = storage.HeightMap[index] * scale;
				var next = height + (center.z - height) * weight;

				storage.HeightMap[index] = (ushort)Math.Clamp( MathF.Round( next / MathF.Max( scale, 0.0001f ) ), 0.0f, ushort.MaxValue );
			}
		}

		var after = storage.HeightMap.ToArray();
		var region = new RectInt( new Vector2Int( 0, 0 ), new Vector2Int( size, size ) );

		Refresh( terrain, region );
		session.AddUndo( "Terrain Pad", () => Restore( terrain, before, region ), () => Restore( terrain, after, region ) );
	}

	static void Restore( Terrain terrain, ushort[] heights, RectInt region )
	{
		heights.CopyTo( terrain.Storage.HeightMap, 0 );
		Refresh( terrain, region );
	}

	static void Refresh( Terrain terrain, RectInt region )
	{
		terrain.Storage.StateHasChanged();
		terrain.ApplyStorageChanges( (Terrain.SyncFlags)(-1), region );
	}

	float Weight( float t )
	{
		t = Math.Clamp( t, 0.0f, 1.0f );

		if ( Falloff <= 0.001f ) return 1.0f;

		var edge = Math.Clamp( (1.0f - t) / Falloff, 0.0f, 1.0f );

		return edge * edge * (3.0f - 2.0f * edge);
	}
}