Code/TerrainSculpt.cs
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.Clutter;

namespace SubZero.SubTerrain;

/// <summary>
/// Runtime heightfield editor: queue CPU stamps, then SyncGPUTexture + region UpdateCollision.
/// Play-only. Add this next to a Terrain component. One sculpt per Terrain.
/// </summary>
[Title( "Terrain Sculpt" )]
[Category( "SubTerrain" )]
[Icon( "landslide" )]
public sealed class TerrainSculpt : Component
{
	const int MinTexelsAcross = 8;

	[Property, Group( "Limits" ), Range( 16f, 512f )]
	public float MaxRadius { get; set; } = 240f;

	[Property, Group( "Limits" ), Range( 0f, 64f )]
	public float FloorMargin { get; set; } = 16f;

	[Property, Group( "Limits" ), Range( 64f, 2048f )]
	public float GroundDrop { get; set; } = 512f;

	[Property, Group( "Crater" )]
	public CraterBrushSettings Crater { get; set; } = new();

	[Property, Group( "Dig" )]
	public DigBrushSettings Dig { get; set; } = new();

	[Property, Group( "Flatten" )]
	public FlattenBrushSettings Flatten { get; set; } = new();

	[Property, Group( "Paint" )]
	public bool PaintDirt { get; set; }

	[Property, Group( "Paint" ), Range( 0, 31 )]
	public int PaintMaterialIndex { get; set; } = 1;

	[Property, Group( "Paint" ), Range( 0f, 1f )]
	public float PaintStrength { get; set; } = 0.85f;

	[Property, Group( "Perf" ), Range( 1, 16 )]
	public int StampsPerFrame { get; set; } = 4;

	[Property, Group( "Debug" )]
	public bool DebugLog { get; set; } = true;

	public Action<TerrainEditResult> OnApplied { get; set; }

	/// <summary>Fired after a successful play-mode reset (Clear Edits / ResetAll). Debris listens here.</summary>
	public Action OnReset { get; set; }

	readonly List<TerrainEditRequest> _queue = new();
	readonly List<TerrainEditResult> _appliedThisFlush = new();
	ushort[] _assetHeight;
	uint[] _assetControl;
	ushort[] _baseHeight;
	uint[] _baseControl;
	float _assetZ;
	bool _floorRaised;
	bool _warnedLowRes;
	bool _paintedThisApply;
	ushort[] _mergeScratch;

	protected override void OnStart()
	{
		if ( !Game.IsPlaying )
			return;

		var ok = EnsureSnapshot();
		Dbg( ok
			? $"snapshot ready res={GetTerrain()?.Storage?.Resolution} size={GetTerrain()?.Storage?.TerrainSize} height={GetTerrain()?.Storage?.TerrainHeight} texels={_baseHeight?.Length}"
			: "snapshot FAILED — no Terrain/Storage on this object" );
	}

	protected override void OnDisabled()
	{
		RestoreFromSnapshot();
	}

	protected override void OnDestroy()
	{
		RestoreFromSnapshot();
	}

	protected override void OnFixedUpdate()
	{
		if ( !Game.IsPlaying )
			return;

		ApplyQueued();
	}

	/// <summary>Queue an edit. Returns whether it was accepted (flushes next fixed update).</summary>
	public static bool TryApply( Scene scene, TerrainEditRequest request )
	{
		if ( scene == null || !scene.IsValid() )
		{
			Log.Warning( "[Sculpt] TryApply — scene invalid" );
			return false;
		}
		if ( !Game.IsPlaying )
		{
			Log.Info( "[Sculpt] TryApply ignored — not playing" );
			return false;
		}
		if ( request.Radius <= 0f )
		{
			Log.Info( $"[Sculpt] TryApply ignored — radius={request.Radius}" );
			return false;
		}

		TerrainSculpt best = null;
		var bestDist = float.MaxValue;
		var candidates = 0;

		foreach ( var sculpt in scene.GetAllComponents<TerrainSculpt>() )
		{
			candidates++;
			if ( !sculpt.Active )
				continue;

			var terrain = sculpt.GetTerrain();
			if ( terrain == null || !terrain.IsValid() || terrain.Storage == null )
			{
				sculpt.Dbg( "TryApply skip — no Terrain/Storage" );
				continue;
			}

			if ( !sculpt.TryProjectToTerrain( terrain, request.WorldPos, out var local ) )
			{
				sculpt.Dbg( $"TryApply miss project pos={request.WorldPos} drop={sculpt.GroundDrop}" );
				continue;
			}

			var hitWorld = terrain.WorldTransform.PointToWorld( local );
			var dist = request.WorldPos.DistanceSquared( hitWorld );
			if ( dist >= bestDist )
				continue;

			bestDist = dist;
			best = sculpt;
		}

		if ( best == null )
		{
			Log.Warning( $"[Sculpt] TryApply — no terrain under {request.WorldPos} (components={candidates})" );
			return false;
		}

		if ( request.Seed == 0 )
			request.Seed = Game.Random.Int( 1, 1_000_000_000 );

		best.Dbg( $"TryApply hit kind={request.Kind} dist={MathF.Sqrt( bestDist ):0.#} pos={request.WorldPos}" );
		best._queue.Add( request );
		return true;
	}

	public static int ResetAll( Scene scene )
	{
		if ( scene == null || !scene.IsValid() )
		{
			Log.Warning( "[Sculpt] ResetAll — scene invalid" );
			return 0;
		}

		var cleared = 0;
		foreach ( var sculpt in scene.GetAllComponents<TerrainSculpt>() )
		{
			if ( sculpt.ResetEdits() )
				cleared++;
		}

		Log.Info( $"[Sculpt] ResetAll — cleared {cleared} terrain(s)" );
		return cleared;
	}

	[Button( "Clear Edits" )]
	public void ClearEdits()
	{
		ResetEdits();
	}

	[Button( "Clear All Scene Edits" )]
	public void ClearAllSceneEdits()
	{
		ResetAll( Scene );
	}

	public bool ResetEdits()
	{
		if ( _baseHeight == null )
		{
			Log.Warning( $"[Sculpt] {GameObject.Name}: Clear Edits — no snapshot (play first, or already restored)" );
			return false;
		}

		var terrain = GetTerrain();
		if ( terrain == null || !terrain.IsValid() || terrain.Storage == null )
		{
			Log.Warning( $"[Sculpt] {GameObject.Name}: Clear Edits — no Terrain/Storage" );
			return false;
		}

		var storage = terrain.Storage;
		if ( storage.HeightMap.Length != _baseHeight.Length )
		{
			Log.Warning( $"[Sculpt] {GameObject.Name}: Clear Edits — size mismatch snapshot={_baseHeight.Length} map={storage.HeightMap.Length}" );
			return false;
		}

		_baseHeight.CopyTo( storage.HeightMap, 0 );
		if ( _baseControl != null && storage.ControlMap.Length == _baseControl.Length )
			_baseControl.CopyTo( storage.ControlMap, 0 );

		PushToEngine( terrain, FullRect( storage.Resolution ), painted: _baseControl != null );
		_queue.Clear();
		OnReset?.Invoke();
		Log.Info( $"[Sculpt] {GameObject.Name}: edits cleared" );
		return true;
	}

	void ApplyQueued()
	{
		if ( _queue.Count == 0 )
			return;

		var terrain = GetTerrain();
		if ( terrain == null || !terrain.IsValid() || terrain.Storage == null )
		{
			Dbg( $"ApplyQueued — dropped {_queue.Count} stamps (no Terrain/Storage)" );
			_queue.Clear();
			return;
		}

		if ( !EnsureSnapshot() )
		{
			Dbg( "ApplyQueued — snapshot failed" );
			return;
		}

		var limit = Math.Clamp( StampsPerFrame, 1, 16 );
		var take = Math.Min( limit, _queue.Count );

		RectInt dirty = default;
		var any = false;
		_paintedThisApply = false;
		_appliedThisFlush.Clear();

		for ( var i = 0; i < take; i++ )
		{
			if ( !ApplyStamp( terrain, _queue[i], out var rect, out var result ) )
				continue;

			_appliedThisFlush.Add( result );
			if ( !any )
			{
				dirty = rect;
				any = true;
			}
			else
			{
				dirty.Add( rect );
			}
		}

		_queue.RemoveRange( 0, take );

		if ( !any )
		{
			Dbg( "ApplyQueued — all stamps missed terrain" );
			return;
		}

		PushToEngine( terrain, dirty, _paintedThisApply );
		Dbg( $"pushed dirty=({dirty.Left},{dirty.Top},{dirty.Width}x{dirty.Height}) painted={_paintedThisApply} remaining={_queue.Count}" );

		var applied = OnApplied;
		if ( applied == null )
			return;

		for ( var i = 0; i < _appliedThisFlush.Count; i++ )
			applied( _appliedThisFlush[i] );
	}

	bool ApplyStamp( Terrain terrain, TerrainEditRequest request, out RectInt dirty, out TerrainEditResult result )
	{
		dirty = default;
		result = default;

		var storage = terrain.Storage;
		if ( storage == null || _baseHeight == null )
			return false;

		var radius = Math.Clamp( request.Radius, 1f, MaxRadius );
		var depth = ResolveDepth( request.Kind, radius, request.Depth );
		if ( request.Kind != TerrainEditKind.Flatten && depth <= 0f )
		{
			Dbg( $"ApplyStamp — depth 0 kind={request.Kind}" );
			return false;
		}

		if ( !TryProjectToTerrain( terrain, request.WorldPos, out var local ) )
		{
			Dbg( $"ApplyStamp — project miss world={request.WorldPos}" );
			return false;
		}

		var size = storage.TerrainSize;
		var height = storage.TerrainHeight;
		var res = storage.Resolution;
		if ( size <= 0f || height <= 0f || res <= 0 )
			return false;

		var uv = new Vector2( local.x, local.y ) / size;
		if ( uv.x < 0f || uv.y < 0f || uv.x > 1f || uv.y > 1f )
			return false;

		var texelSize = size / res;
		var across = radius * 2f / texelSize;
		if ( across < MinTexelsAcross && !_warnedLowRes )
		{
			_warnedLowRes = true;
			Log.Warning( $"[TerrainSculpt] Radius {radius:0} spans {across:0.0} texels (need >={MinTexelsAcross} to hide a character). texelSize={texelSize:0.0}." );
		}

		var cx = (int)Math.Floor( res * uv.x );
		var cy = (int)Math.Floor( res * uv.y );
		var irreg = request.Kind switch
		{
			TerrainEditKind.Dig => Math.Clamp( Dig.Irregularity, 0f, 0.4f ),
			TerrainEditKind.Flatten => 0f,
			_ => Math.Clamp( Crater.Irregularity, 0f, 0.7f )
		};
		var stretchAmt = request.Kind switch
		{
			TerrainEditKind.Dig => Math.Clamp( Dig.Stretch, 0f, 0.35f ),
			TerrainEditKind.Flatten => 0f,
			_ => Math.Clamp( Crater.Stretch, 0f, 0.5f )
		};
		var blend = request.Kind switch
		{
			TerrainEditKind.Dig => Math.Clamp( Dig.Blend, 0f, 1f ),
			TerrainEditKind.Flatten => 0f,
			_ => Math.Clamp( Crater.Blend, 0f, 1f )
		};
		var shapePad = 1f + irreg + stretchAmt + 0.25f + blend * 0.15f;
		var radTexels = Math.Max( 1, (int)Math.Ceiling( radius / texelSize * shapePad ) + 3 );
		dirty = new RectInt( cx - radTexels, cy - radTexels, radTexels * 2 + 1, radTexels * 2 + 1 );

		var x0 = Math.Clamp( dirty.Left, 0, res - 1 );
		var y0 = Math.Clamp( dirty.Top, 0, res - 1 );
		var x1 = Math.Clamp( dirty.Right, 0, res );
		var y1 = Math.Clamp( dirty.Bottom, 0, res );
		if ( x1 <= x0 || y1 <= y0 )
			return false;

		dirty = new RectInt( x0, y0, x1 - x0, y1 - y0 );

		var crater = Crater;
		var dig = Dig;
		var maxDepth = request.Kind == TerrainEditKind.Dig ? dig.MaxDepth : crater.MaxDepth;
		var maxRaise = request.Kind == TerrainEditKind.Dig ? dig.MaxRaise : crater.MaxRaise;
		var bowlFrac = request.Kind == TerrainEditKind.Dig
			? Math.Clamp( dig.BowlRadius, 0.7f, 0.98f )
			: Math.Clamp( crater.BowlRadius, 0.4f, 0.95f );

		var depthU = WorldToUshort( depth, height );
		var maxDepthU = WorldToUshort( maxDepth, height );
		var raiseU = WorldToUshort( maxRaise, height );

		var heights = storage.HeightMap;
		var control = storage.ControlMap;
		var paint = PaintDirt && PaintMaterialIndex >= 0
			&& storage.Materials != null
			&& PaintMaterialIndex < storage.Materials.Count;

		if ( paint )
			EnsureControlSnapshot();

		var originX = uv.x * res;
		var originY = uv.y * res;
		var radiusTexels = radius / texelSize;
		if ( radiusTexels < 0.5f )
			return false;

		var rng = new Random( request.Seed == 0 ? 1 : request.Seed );
		var rot = rng.Float( 0f, MathF.PI * 2f );
		var stretchX = Math.Clamp( 1f + rng.Float( -stretchAmt, stretchAmt ), 0.55f, 1.7f );
		if ( stretchAmt <= 0.001f )
			stretchX = 1f;

		var sampleX = Math.Clamp( cx, x0, x1 - 1 );
		var sampleY = Math.Clamp( cy, y0, y1 - 1 );
		var sampleI = sampleX + sampleY * res;
		var heightBefore = heights[sampleI];

		var flattenTargetU = (int)heightBefore;
		if ( request.Kind == TerrainEditKind.Flatten && request.HasTargetZ )
		{
			var targetLocal = terrain.WorldTransform.PointToLocal( new Vector3( request.WorldPos.x, request.WorldPos.y, request.TargetZ ) );
			flattenTargetU = WorldToUshort( targetLocal.z, height );
		}

		var ctx = new TerrainBrushes.Context
		{
			Heights = heights,
			Base = _baseHeight,
			Control = control,
			Res = res,
			X0 = x0,
			Y0 = y0,
			X1 = x1,
			Y1 = y1,
			OriginX = originX,
			OriginY = originY,
			RadiusTexels = radiusTexels,
			Ca = MathF.Cos( rot ),
			Sa = MathF.Sin( rot ),
			StretchX = stretchX,
			StretchY = 1f / stretchX,
			Ox = rng.Float( 0f, 64f ),
			Oy = rng.Float( 0f, 64f ),
			Oz = rng.Float( 0f, 64f ),
			Freq = 2.1f + rng.Float( 0f, 2.4f ),
			Irreg = irreg,
			DepthU = depthU,
			MaxDepthU = maxDepthU,
			RaiseU = raiseU,
			BowlFrac = bowlFrac,
			SmoothK = blend * depthU * 0.55f,
			DugU = Math.Max( 2, WorldToUshort( 8f, height ) ),
			Paint = paint,
			PaintIndex = PaintMaterialIndex,
			PaintStrength = PaintStrength,
			FlattenTargetU = flattenTargetU,
			FlattenStrength = Math.Clamp( Flatten.Strength, 0.05f, 1f ),
			FlattenMaxDeltaU = WorldToUshort( Flatten.MaxDelta, height ),
			SpoilAngle = rng.Float( 0f, MathF.PI * 2f )
		};

		if ( request.Kind != TerrainEditKind.Flatten )
		{
			var eccA = rng.Float( 0f, MathF.PI * 2f );
			var ecc = rng.Float( 0f, irreg * 0.22f ) * radiusTexels;
			ctx.OriginX = originX + MathF.Cos( eccA ) * ecc;
			ctx.OriginY = originY + MathF.Sin( eccA ) * ecc;
		}

		switch ( request.Kind )
		{
			case TerrainEditKind.Dig:
				TerrainBrushes.ApplyDig( ref ctx );
				break;
			case TerrainEditKind.Flatten:
				TerrainBrushes.ApplyFlatten( ref ctx );
				break;
			default:
				TerrainBrushes.ApplyCrater( ref ctx );
				break;
		}

		if ( ctx.Painted )
			_paintedThisApply = true;

		if ( blend > 0.01f && request.Kind != TerrainEditKind.Flatten )
			BreakSaddles( storage, x0, y0, x1, y1, maxDepthU, ctx.DugU, blend, paint );

		var heightAfter = heights[sampleI];
		var dropWorld = (heightBefore - heightAfter) / (float)ushort.MaxValue * height;
		var volume = ctx.VolumeU / (float)ushort.MaxValue * height * texelSize * texelSize;
		Dbg( $"stamp kind={request.Kind} uv={uv.x:0.000},{uv.y:0.000} texel={cx},{cy} across={across:0.0} r={radius:0.#} d={depth:0.#} blend={blend:0.00} center {heightBefore}->{heightAfter} drop={dropWorld:0.0}u wrote={ctx.Written} rect=({dirty.Left},{dirty.Top},{dirty.Width}x{dirty.Height})" );

		result = new TerrainEditResult
		{
			Kind = request.Kind,
			Origin = request.WorldPos,
			Radius = radius,
			Depth = depth,
			Dirty = dirty,
			Texels = ctx.Written,
			VolumeRemoved = volume
		};
		return true;
	}

	void BreakSaddles( TerrainStorage storage, int x0, int y0, int x1, int y1, int maxDepthU, int dugU, float blend, bool paint )
	{
		if ( _baseHeight == null )
			return;

		var res = storage.Resolution;
		var w = x1 - x0;
		var hgt = y1 - y0;
		if ( w < 3 || hgt < 3 )
			return;

		var need = w * hgt;
		if ( _mergeScratch == null || _mergeScratch.Length < need )
			_mergeScratch = new ushort[need];

		var heights = storage.HeightMap;
		var control = storage.ControlMap;
		var scratch = _mergeScratch;

		for ( var y = 0; y < hgt; y++ )
		{
			var src = (y0 + y) * res + x0;
			Array.Copy( heights, src, scratch, y * w, w );
		}

		for ( var y = 1; y < hgt - 1; y++ )
		{
			for ( var x = 1; x < w - 1; x++ )
			{
				var i = (y0 + y) * res + (x0 + x);
				var row = y * w + x;
				var left = scratch[row - 1];
				var right = scratch[row + 1];
				var down = scratch[row - w];
				var up = scratch[row + w];
				var leftD = (int)_baseHeight[i - 1] - left >= dugU;
				var rightD = (int)_baseHeight[i + 1] - right >= dugU;
				var downD = (int)_baseHeight[i - res] - down >= dugU;
				var upD = (int)_baseHeight[i + res] - up >= dugU;
				if ( !((leftD && rightD) || (downD && upD)) )
					continue;

				var minN = int.MaxValue;
				if ( leftD ) minN = Math.Min( minN, left );
				if ( rightD ) minN = Math.Min( minN, right );
				if ( downD ) minN = Math.Min( minN, down );
				if ( upD ) minN = Math.Min( minN, up );

				var self = scratch[row];
				if ( minN >= self )
					continue;

				var b = (int)_baseHeight[i];
				var merged = (int)MathF.Round( self + (minN - self) * blend );
				merged = Math.Max( merged, b - maxDepthU );
				heights[i] = (ushort)Math.Clamp( merged, 0, ushort.MaxValue );

				if ( !paint || control == null )
					continue;

				var mat = new CompactTerrainMaterial( control[i] );
				if ( mat.IsHole )
					continue;

				mat.OverlayTextureId = (byte)PaintMaterialIndex;
				mat.BlendFactor = (byte)Math.Clamp( mat.BlendFactor + (int)(blend * 80f), 0, 255 );
				control[i] = mat.Packed;
				_paintedThisApply = true;
			}
		}
	}

	bool TryProjectToTerrain( Terrain terrain, Vector3 worldPos, out Vector3 local )
	{
		local = default;
		var origin = worldPos + Vector3.Up * 8f;
		var ray = new Ray( origin, Vector3.Down );
		if ( terrain.RayIntersects( ray, GroundDrop + 8f, out local ) )
			return true;

		local = terrain.WorldTransform.PointToLocal( worldPos );
		var size = terrain.Storage?.TerrainSize ?? 0f;
		return local.x >= 0f && local.y >= 0f && local.x <= size && local.y <= size;
	}

	void PushToEngine( Terrain terrain, RectInt region, bool painted )
	{
		if ( region.Width < 1 || region.Height < 1 )
			return;

		if ( terrain.HeightMap != null )
			terrain.SyncGPUTexture();

		var flags = Terrain.SyncFlags.Height;
		if ( painted )
			flags |= Terrain.SyncFlags.Control;

		terrain.UpdateCollision( flags, region );
		InvalidateClutter( terrain, region );
	}

	void InvalidateClutter( Terrain terrain, RectInt region )
	{
		if ( !Scene.IsValid() )
			return;

		var clutter = Scene.GetSystem<ClutterGridSystem>();
		if ( clutter == null )
			return;

		var storage = terrain.Storage;
		if ( storage == null )
			return;

		var res = storage.Resolution;
		var size = storage.TerrainSize;
		var minLocal = new Vector3( region.Left / (float)res * size, region.Top / (float)res * size, -1000f );
		var maxLocal = new Vector3( region.Right / (float)res * size, region.Bottom / (float)res * size, 1000f );
		var a = terrain.WorldTransform.PointToWorld( minLocal );
		var b = terrain.WorldTransform.PointToWorld( maxLocal );
		clutter.InvalidateTilesInBounds( new BBox( a, b ) );
	}

	bool EnsureSnapshot()
	{
		var terrain = GetTerrain();
		if ( terrain == null || !terrain.IsValid() || terrain.Storage?.HeightMap == null )
			return false;

		var map = terrain.Storage.HeightMap;
		if ( _baseHeight != null && _baseHeight.Length == map.Length )
			return true;

		_assetZ = WorldPosition.z;
		_assetHeight = new ushort[map.Length];
		map.CopyTo( _assetHeight, 0 );
		_baseHeight = new ushort[map.Length];
		map.CopyTo( _baseHeight, 0 );
		SnapshotControl();
		RaiseFloor( terrain );
		return true;
	}

	void SnapshotControl()
	{
		var terrain = GetTerrain();
		if ( terrain?.Storage?.ControlMap == null )
			return;

		var control = terrain.Storage.ControlMap;
		_assetControl = new uint[control.Length];
		control.CopyTo( _assetControl, 0 );
		_baseControl = new uint[control.Length];
		control.CopyTo( _baseControl, 0 );
	}

	void EnsureControlSnapshot()
	{
		if ( _baseControl != null )
			return;

		SnapshotControl();
	}

	void RaiseFloor( Terrain terrain )
	{
		var storage = terrain.Storage;
		if ( storage == null || _baseHeight == null )
			return;

		var height = storage.TerrainHeight;
		var needWorld = MathF.Max( Crater.MaxDepth, Dig.MaxDepth ) + FloorMargin;
		var needU = WorldToUshort( needWorld, height );

		var min = ushort.MaxValue;
		for ( var i = 0; i < _baseHeight.Length; i++ )
		{
			if ( _baseHeight[i] < min )
				min = _baseHeight[i];
		}

		if ( min >= needU )
		{
			Dbg( $"floor ok min={min} need={needU} ({needWorld:0.#}u of {height:0.#})" );
			return;
		}

		var pad = needU - min;
		for ( var i = 0; i < _baseHeight.Length; i++ )
		{
			var v = (int)_baseHeight[i] + pad;
			_baseHeight[i] = (ushort)Math.Min( v, ushort.MaxValue );
		}

		_baseHeight.CopyTo( storage.HeightMap, 0 );

		var padWorld = pad / (float)ushort.MaxValue * height;
		WorldPosition = WorldPosition.WithZ( _assetZ - padWorld );
		_floorRaised = true;

		PushToEngine( terrain, FullRect( storage.Resolution ), painted: false );
		Dbg( $"raised floor pad={pad} ({padWorld:0.#}u) min {min}->{min + pad} need={needU} z {_assetZ:0.#}->{WorldPosition.z:0.#}" );
	}

	void RestoreFromSnapshot()
	{
		if ( _assetHeight == null )
			return;

		var terrain = GetTerrain();
		if ( terrain == null || !terrain.IsValid() || terrain.Storage == null )
			return;

		var storage = terrain.Storage;
		if ( storage.HeightMap.Length != _assetHeight.Length )
			return;

		_assetHeight.CopyTo( storage.HeightMap, 0 );
		if ( _assetControl != null && storage.ControlMap.Length == _assetControl.Length )
			_assetControl.CopyTo( storage.ControlMap, 0 );

		if ( _floorRaised )
		{
			WorldPosition = WorldPosition.WithZ( _assetZ );
			_floorRaised = false;
		}

		PushToEngine( terrain, FullRect( storage.Resolution ), painted: _assetControl != null );
		_queue.Clear();
		_assetHeight = null;
		_assetControl = null;
		_baseHeight = null;
		_baseControl = null;
		Dbg( "RestoreFromSnapshot — disk heightmap restored" );
	}

	float ResolveDepth( TerrainEditKind kind, float radius, float depth )
	{
		if ( kind == TerrainEditKind.Flatten )
			return 0f;

		var scale = kind == TerrainEditKind.Dig ? Dig.DepthScale : Crater.DepthScale;
		var max = kind == TerrainEditKind.Dig ? Dig.MaxDepth : Crater.MaxDepth;
		var d = depth > 0f ? depth : radius * scale;
		return Math.Clamp( d, 0f, max );
	}

	Terrain GetTerrain()
	{
		return Components.Get<Terrain>( FindMode.EverythingInSelfAndDescendants );
	}

	static RectInt FullRect( int resolution ) => new( 0, 0, resolution, resolution );

	static int WorldToUshort( float world, float terrainHeight )
	{
		if ( terrainHeight <= 0f )
			return 0;
		return (int)Math.Clamp( MathF.Round( world / terrainHeight * ushort.MaxValue ), 0f, (float)ushort.MaxValue );
	}

	void Dbg( string message )
	{
		if ( !DebugLog )
			return;

		Log.Info( $"[Sculpt] {GameObject.Name}: {message}" );
	}
}