Code/Water/Modes/Tiles/WaterController.WaterTiles.Gizmo.cs

Editor gizmo code for WaterController that draws and lets the user edit a tiled water region. It snaps a user-entered bounding box to a tile grid, draws wire and solid boxes (with xray pass), provides interactive handles, and rescales bounds when tile size changes.

Native Interop
namespace AtmokineticAssets;

public sealed partial class WaterController : Component
{
	private void DrawWaterTileGizmo()
	{
		if ( !EnableWaterTileGizmo )
			return;

		if ( !Gizmo.IsSelected && !Gizmo.IsHovered )
			return;

		// Raw box from the user's typed bounds — used for the editing handles.
		var rawBox = new BBox(
			UnitConversion.CentimetersToInches( WaterTileBoundsMinimum ),
			UnitConversion.CentimetersToInches( WaterTileBoundsMaximum ) );

		// Snapped box — the actual world-grid region the tiles fill — used for drawing.
		var snappedBox = GetSnappedLocalBox( rawBox );

		DrawWaterTileBounds( snappedBox );
		DrawWaterTileHandles( rawBox );
	}

	// Snaps the bounds extent to the tile grid relative to the GameObject's own origin,
	// so the box keeps its tile-sized footprint but follows the GameObject smoothly (no world-grid jumping).
	private BBox GetSnappedLocalBox( BBox localBox )
	{
		float tileSizeInches = UnitConversion.CentimetersToInches( WaterTileSize );
		if ( tileSizeInches <= 0 )
			return localBox;

		float minX = MathF.Floor( MathF.Round( localBox.Mins.x / tileSizeInches, 4 ) ) * tileSizeInches;
		float minY = MathF.Floor( MathF.Round( localBox.Mins.y / tileSizeInches, 4 ) ) * tileSizeInches;
		float maxX = MathF.Ceiling( MathF.Round( localBox.Maxs.x / tileSizeInches, 4 ) ) * tileSizeInches;
		float maxY = MathF.Ceiling( MathF.Round( localBox.Maxs.y / tileSizeInches, 4 ) ) * tileSizeInches;

		return new BBox(
			new Vector3( minX, minY, localBox.Mins.z ),
			new Vector3( maxX, maxY, localBox.Maxs.z ) );
	}

	private void DrawWaterTileBounds( BBox box )
	{
		Gizmo.Draw.LineThickness = 1;
		var tint = Gizmo.IsSelected ? Gizmo.Colors.Blue : Gizmo.Colors.Blue.WithAlpha( 0.4f );

		for ( int i = 0; i < 2; i++ )
		{
			bool xray = i == 0;
			Gizmo.Draw.IgnoreDepth = xray;
			float alpha = xray ? 0.2f : 1f;

			Gizmo.Draw.Color = tint.WithAlpha( tint.a * alpha );
			Gizmo.Draw.LineBBox( box );

			Gizmo.Draw.Color = tint.WithAlpha( 0.05f * alpha );
			Gizmo.Draw.SolidBox( box );
		}
	}
	
	private void DrawWaterTileHandles( BBox box )
	{
		Gizmo.Hitbox.BBox( box );

		// Disable the editor's grid snap for this control so it doesn't double up with our tile snap.
		var previousSnap = Gizmo.Settings.SnapToGrid;
		Gizmo.Settings.SnapToGrid = false;

		if ( Gizmo.Control.BoundingBox( "water_tile_bounds", box, out var newBox ) )
		{
			var minCm = UnitConversion.InchesToCentimeters( newBox.Mins );
			var maxCm = UnitConversion.InchesToCentimeters( newBox.Maxs );

			WaterTileBoundsMinimum = SnapChangedAxes( WaterTileBoundsMinimum, minCm, WaterTileSize, floor: true );
			WaterTileBoundsMaximum = SnapChangedAxes( WaterTileBoundsMaximum, maxCm, WaterTileSize, floor: false );
		}

		Gizmo.Settings.SnapToGrid = previousSnap;
	}

	private float ResizeWaterTileBounds( float oldSize, float newSize )
	{
		// Skip during deserialization / before start — only rescale on genuine edits.
		if ( !_tilesStarted || oldSize == newSize || newSize <= 0 || oldSize <= 0 )
			return newSize;

		float scale = newSize / oldSize;

		// Scale X/Y of the bounds so the box stays the same tile count.
		WaterTileBoundsMinimum = new Vector3( WaterTileBoundsMinimum.x * scale, WaterTileBoundsMinimum.y * scale, WaterTileBoundsMinimum.z );
		WaterTileBoundsMaximum = new Vector3( WaterTileBoundsMaximum.x * scale, WaterTileBoundsMaximum.y * scale, WaterTileBoundsMaximum.z );

		return newSize;
	}

	private static Vector3 SnapChangedAxes( Vector3 oldValue, Vector3 newValue, float tileSize, bool floor )
	{
		if ( tileSize <= 0 )
			return newValue;

		float SnapAxis( float n ) =>
			( floor ? MathF.Floor( n / tileSize ) : MathF.Ceiling( n / tileSize ) ) * tileSize;

		return new Vector3(
			oldValue.x.AlmostEqual( newValue.x ) ? oldValue.x : SnapAxis( newValue.x ),
			oldValue.y.AlmostEqual( newValue.y ) ? oldValue.y : SnapAxis( newValue.y ),
			oldValue.z.AlmostEqual( newValue.z ) ? oldValue.z : newValue.z );
	}
}