Editor/FoliagePainter.Erase.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Foliage;
public partial class FoliagePainter
{
private void Erase( SceneTraceResult paintTrace )
{
if ( Settings.ContainerObject is null ) return;
// Update brush rotation if RotateBrush is enabled
if ( Settings.BrushMode == BrushMode.Texture && Settings.RotateBrushOnPaint )
{
var variance = Random.Shared.Float( -Settings.RotationAmountVariance, Settings.RotationAmountVariance );
Settings.CurrentBrushRotation = Settings.RotationAmount + variance;
}
var didErase = false;
// Try up to 10 times to find an object to erase
for ( int attempt = 0; attempt < 50; attempt++ )
{
if ( TryErase( paintTrace ) )
{
didErase = true;
break;
}
}
if ( !didErase )
{
Log.Info( "Failed to erase any foliage" );
}
}
private bool TryErase( SceneTraceResult paintTrace )
{
var container = Settings.ContainerObject;
List<GameObject> candidateObjects = [];
if ( container is null )
{
// Find all objects that have FoliageInfo in the scene
candidateObjects = Scene.GetAllObjects( true ).Where( obj => obj.Components.TryGet<FoliageInfo>( out _ ) ).ToList();
}
else
{
candidateObjects = [.. container.Children];
}
if ( candidateObjects.Count == 0 )
return false;
// Get a random position within the brush
Vector3 erasePosition;
if ( Settings.BrushMode == BrushMode.Texture )
{
if ( Settings.Brush is null ) return false;
var randomBrushPixel = GetRandomBrushPixel();
// Rotate UV coordinates around center (0.5, 0.5) if RotateBrush is enabled
if ( Settings.RotateBrushOnPaint && Settings.CurrentBrushRotation != 0f )
{
var center = new Vector2( 0.5f, 0.5f );
var offset = randomBrushPixel - center;
var angleRad = Settings.CurrentBrushRotation * MathF.PI / 180f;
var cos = MathF.Cos( angleRad );
var sin = MathF.Sin( angleRad );
var rotatedOffset = new Vector2(
offset.x * cos - offset.y * sin,
offset.x * sin + offset.y * cos
);
randomBrushPixel = center + rotatedOffset;
}
var scaledRandomPosition = new Vector3( randomBrushPixel.x, randomBrushPixel.y, 0 ) * Settings.Size * 2 - new Vector3( Settings.Size, Settings.Size, 0 );
erasePosition = paintTrace.HitPosition + scaledRandomPosition;
}
else
{
var angle = Random.Shared.Float( 0f, MathF.Tau );
var radius = MathF.Sqrt( Random.Shared.Float( 0f, 1f ) ) * Settings.Size;
var a = MathF.Abs( paintTrace.Normal.z ) < 0.999f ? Vector3.Up : Vector3.Left;
var u = paintTrace.Normal.Cross( a ).Normal;
var v = paintTrace.Normal.Cross( u ).Normal;
erasePosition = paintTrace.HitPosition + radius * (u * MathF.Cos( angle ) + v * MathF.Sin( angle ));
}
// Filter by distance (within search radius from erase position)
var searchRadiusSquared = Settings.EraseSearchRadius * Settings.EraseSearchRadius;
var objectsInRange = candidateObjects
.Where( obj =>
{
var distanceSquared = Vector3.DistanceBetweenSquared( erasePosition, obj.WorldPosition );
return distanceSquared <= searchRadiusSquared;
} )
.ToList();
if ( objectsInRange.Count == 0 )
return false;
// Filter by palette if EraseOnlyPalette is enabled
if ( Settings.EraseOnlyPalette && Settings.Palette is not null )
{
objectsInRange = objectsInRange
.Where( obj =>
{
if ( !obj.Components.TryGet<FoliageInfo>( out var foliageInfo ) )
return false;
return foliageInfo.PaletteResourceId == Settings.Palette.ResourceId;
} )
.ToList();
if ( objectsInRange.Count == 0 )
return false;
}
// Pick a random object and delete it
var objectToErase = Game.Random.FromList( objectsInRange );
if ( objectToErase is not null && objectToErase.IsValid() )
{
objectToErase.Destroy();
return true;
}
return false;
}
}