🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (33 total matches found)
subzerostudios.subterrain-tool / Editor/SubTerrainSculptMode.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

namespace Editor.SubTerrain;

/// <summary>
/// Sculpt modes for the SubTerrain compute shader.
/// Values 0-4 match the engine shader; 5-8 are SubTerrain extensions.
/// </summary>
public enum SubTerrainSculptMode
{
	RaiseLower = 0,
	Flatten = 1,
	Smooth = 2,
	Hole = 3,
	Noise = 4,
	Slope = 5,
	Path = 6,
	Plainer = 7,
	Bridge = 8
}
subzerostudios.subterrain-tool / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "SubTerrain Tool" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "subterrain-tool" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "subzerostudios" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "subzerostudios.subterrain-tool" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-08-24T22:24:58.1339886Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.165.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.165.0")]
subzerostudios.subterrain-tool / Code/TerrainEditRequest.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using Sandbox;

namespace SubZero.SubTerrain;

public struct TerrainEditRequest
{
	public TerrainEditKind Kind;
	public Vector3 WorldPos;
	public float Radius;

	/// <summary>0 = kind auto-scale (radius × DepthScale, clamped).</summary>
	public float Depth;

	/// <summary>Unused this pass. Reserved for a later trench (capsule SDF).</summary>
	public Vector3 EndPos;

	public bool HasTargetZ;
	public float TargetZ;
	public int Seed;
}

public struct TerrainEditResult
{
	public TerrainEditKind Kind;
	public Vector3 Origin;
	public float Radius;
	public float Depth;
	public RectInt Dirty;
	public int Texels;
	public float VolumeRemoved;
}
subzerostudios.subterrain-tool / Editor/SubTerrainBlendWidget.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;

namespace Editor.SubTerrain;

/// <summary>
/// Paint Texture sidebar: tmat list, Ground / Wear / Path, Steep / All, live mix.
/// </summary>
internal class SubTerrainBlendWidget : Widget
{
	readonly SubTerrainTool _tool;
	readonly SubTerrainPaintModeStrip _modes;
	readonly SubTerrainPairPreview _pairPreview;
	readonly Widget _listHost;
	readonly Widget _optionsHost;
	readonly Label _live;
	Widget _limitRow;
	Checkbox _keepPairBox;
	Checkbox _sampledBox;
	Checkbox _packCenterBox;
	int _listedCount = -1;
	SubTerrainPaintLayer _sheetLayer = (SubTerrainPaintLayer)(-1);
	bool _sheetSteep;

	const string IntentSuffix = " Alt sample | Ctrl fade | Shift smooth | ring yellow/green = Alt/Shift.";

	public SubTerrainBlendWidget( SubTerrainTool tool, Widget parent ) : base( parent )
	{
		_tool = tool;
		Layout = Layout.Column();
		Layout.Spacing = 4;

		_modes = Layout.Add( new SubTerrainPaintModeStrip( tool, this ) );
		_pairPreview = Layout.Add( new SubTerrainPairPreview( tool, this ) );
		SyncPairPreview();
		_listHost = Layout.Add( new Widget( this ) );
		_listHost.Layout = Layout.Column();
		_listHost.Layout.Spacing = 0;
		_optionsHost = Layout.Add( new Widget( this ) );
		_optionsHost.Layout = Layout.Column();
		_optionsHost.Layout.Spacing = 4;
		_live = Layout.Add( new Label( "" ) );

		RebuildList();
		RebuildOptions();
		RefreshCopy();
	}

	void OnGroundClicked() => SetLayer( SubTerrainPaintLayer.Base );

	void OnWearClicked() => SetLayer( SubTerrainPaintLayer.Overlay );

	void OnPathClicked()
	{
		var terrain = _tool.PeekTerrain();
		int ground = SubTerrainSplatUtil.FindPathGround( terrain, _tool.PaintSettings );
		int wear = _tool.PaintSettings.MaterialIndex;
		if ( wear == ground )
		{
			wear = SubTerrainSplatUtil.FindPathWear( terrain, ground );
			_tool.PaintSettings.MaterialIndex = wear;
			SubTerrainPaintTool.TrySetSplatChannel( wear );
		}

		SetLayer( SubTerrainPaintLayer.Path );
	}

	void SetLayer( SubTerrainPaintLayer layer )
	{
		SubTerrainPaintStore.Capture( _tool.PaintSettings );
		SubTerrainPaintStore.Apply( _tool.PaintSettings, layer );
		SubTerrainPaintTool.ActiveLayer = layer;
		RebuildOptions();
		_tool.RefreshPaintChrome();
		RefreshCopy();
		_modes.Update();
		SyncPairPreview();
	}

	void SyncPairPreview()
	{
		if ( _pairPreview is null )
			return;

		// Ground writes one tmat - a pair thumb is dead chrome.
		bool show = _tool.PaintSettings.Layer != SubTerrainPaintLayer.Base;
		_pairPreview.Visible = show;
		_pairPreview.FixedHeight = show ? 36 : 0;
		_pairPreview.MinimumHeight = show ? 36 : 0;
		_pairPreview.MaximumHeight = show ? 36 : 0;
		if ( show )
			_pairPreview.Update();
	}

	internal void SelectGround() => OnGroundClicked();
	internal void SelectWear() => OnWearClicked();
	internal void SelectPath() => OnPathClicked();

	internal void OnMaterialSelectionChanged()
	{
		RefreshCopy();
		SyncPairPreview();
		_listHost?.Update();
		if ( _listHost?.Layout is null )
			return;

		foreach ( var child in _listHost.Children )
			child?.Update();
	}

	internal void SelectSteep()
	{
		_tool.PaintSettings.OnlyOnSlope = true;
		RebuildOptions();
		RefreshCopy();
	}

	internal void SelectAll()
	{
		_tool.PaintSettings.OnlyOnSlope = false;
		RebuildOptions();
		RefreshCopy();
	}

	void OnKeepPairToggled()
	{
		if ( _keepPairBox is null )
			return;

		_tool.PaintSettings.MixOnly = _keepPairBox.Value;
		RefreshCopy();
	}

	void OnSampledToggled()
	{
		if ( _sampledBox is null )
			return;

		_tool.PaintSettings.LimitToSampledGround = _sampledBox.Value;
		RefreshCopy();
	}

	void OnPackCenterToggled()
	{
		if ( _packCenterBox is null )
			return;

		_tool.PaintSettings.PackCenter = _packCenterBox.Value;
		RebuildOptions();
		RefreshCopy();
		SyncPairPreview();
	}

	void RebuildList()
	{
		if ( _listHost is null )
			return;

		_listHost.Layout ??= Layout.Column();
		_listHost.Layout.Clear( true );

		var terrain = _tool.PeekTerrain();
		var materials = terrain?.Storage?.Materials;
		int count = materials?.Count ?? 0;
		_listedCount = count;

		if ( count == 0 )
		{
			_listHost.Layout.Add( new Label( "This terrain has no materials to paint." ) );
			return;
		}

		for ( int i = 0; i < count; i++ )
		{
			var name = SubTerrainSplatUtil.MaterialName( terrain, i );
			_listHost.Layout.Add( new SubTerrainMaterialRow( _tool, i, name, _listHost ) );
		}
	}

	void RebuildOptions()
	{
		if ( _optionsHost is null )
			return;

		_optionsHost.Layout ??= Layout.Column();
		_optionsHost.Layout.Clear( true );
		_limitRow = null;
		_keepPairBox = null;
		_sampledBox = null;
		_packCenterBox = null;
		_sheetLayer = _tool.PaintSettings.Layer;
		_sheetSteep = _tool.PaintSettings.OnlyOnSlope;

		var so = _tool.PaintSettings.GetSerialized();
		if ( so is null )
			return;

		if ( _sheetLayer == SubTerrainPaintLayer.Overlay )
			_optionsHost.Layout.Add( new SubTerrainWhereStrip( _tool, this ) );

		var sheet = new ControlSheet { IncludePropertyNames = false };
		if ( _sheetLayer == SubTerrainPaintLayer.Overlay )
			AddSettingRow( sheet, so, nameof( SubTerrainPaintSettings.HowWorn ) );
		else if ( _sheetLayer == SubTerrainPaintLayer.Path )
			AddSettingRow( sheet, so, nameof( SubTerrainPaintSettings.CenterMix ) );

		if ( _sheetLayer == SubTerrainPaintLayer.Overlay && _sheetSteep )
			AddSettingRow( sheet, so, nameof( SubTerrainPaintSettings.SlopeDegrees ) );

		_optionsHost.Layout.Add( sheet );

		if ( _sheetLayer == SubTerrainPaintLayer.Overlay )
		{
			_keepPairBox = new Checkbox( "Keep pair" );
			_keepPairBox.Value = _tool.PaintSettings.MixOnly;
			_keepPairBox.ToolTip = "Only move mix. Do not change the base/overlay pair (avoids texel stairs next to a different pair).";
			_keepPairBox.Toggled += OnKeepPairToggled;
			_optionsHost.Layout.Add( _keepPairBox );

			_sampledBox = new Checkbox( "Stay on sampled" );
			_sampledBox.Value = _tool.PaintSettings.LimitToSampledGround;
			_sampledBox.ToolTip = "Alt-click locks to that ground base. Wear will not paint other bases.";
			_sampledBox.Toggled += OnSampledToggled;
			_optionsHost.Layout.Add( _sampledBox );
			_limitRow = _sampledBox;
		}
		else if ( _sheetLayer == SubTerrainPaintLayer.Path )
		{
			_packCenterBox = new Checkbox( "Pack center" );
			_packCenterBox.Value = _tool.PaintSettings.PackCenter;
			_packCenterBox.ToolTip = "On: solid path core along the stroke, soft apron outside. Off: soft ground + path apron only.";
			_packCenterBox.Toggled += OnPackCenterToggled;
			_optionsHost.Layout.Add( _packCenterBox );

			if ( _tool.PaintSettings.PackCenter )
			{
				var coreSheet = new ControlSheet { IncludePropertyNames = false };
				AddSettingRow( coreSheet, so, nameof( SubTerrainPaintSettings.PackCore ) );
				_optionsHost.Layout.Add( coreSheet );
			}
		}
	}

	static void AddSettingRow( ControlSheet sheet, SerializedObject so, string name )
	{
		if ( so.TryGetProperty( name, out var prop ) && prop is not null )
			sheet.AddRow( prop );
	}

	void RefreshCopy()
	{
		if ( _live is null )
			return;

		var line = WriteLine();
		if ( _tool.PaintSettings.Layer == SubTerrainPaintLayer.Overlay && _tool.PaintSettings.Dithers )
			line += " | dither";

		_live.Text = line;
		_live.ToolTip = IntentTip();
		_live.Color = Theme.TextControl;
	}

	string WriteLine()
	{
		var settings = _tool.PaintSettings;
		var layer = settings.Layer;
		var terrain = _tool.PeekTerrain();
		string mix = settings.MixCap.ToString( "0.00" );

		if ( layer == SubTerrainPaintLayer.Path )
		{
			int ground = SubTerrainSplatUtil.FindPathGround( terrain, settings );
			int wear = settings.MaterialIndex;
			if ( ground == wear )
				return "Click the packed tmat. Alt-click the ground.";

			var gName = SubTerrainSplatUtil.MaterialName( terrain, ground );
			var wName = SubTerrainSplatUtil.MaterialName( terrain, wear );
			return settings.PackCenter
				? $"{wName} center | {gName} | {wName} {mix} | core {settings.PackCore:0.00}"
				: $"{gName} | {wName} {mix} | soft";
		}

		if ( layer == SubTerrainPaintLayer.Base )
			return SubTerrainSplatUtil.MaterialName( terrain, settings.MaterialIndex );

		var name = SubTerrainSplatUtil.MaterialName( terrain, settings.MaterialIndex );
		string lockOn = "";
		if ( settings.LimitToSampledGround && settings.HasSampledGround )
			lockOn = $" on {SubTerrainSplatUtil.MaterialName( terrain, settings.SampledGroundIndex )}";

		if ( settings.MixOnly )
			return $"mix {name} {mix}{lockOn}";

		return settings.OnlyOnSlope
			? $"{name} {mix} steep{lockOn}"
			: $"{name} {mix}{lockOn}";
	}

	string IntentTip()
	{
		var settings = _tool.PaintSettings;
		string body = settings.Layer switch
		{
			SubTerrainPaintLayer.Path => settings.PackCenter
				? "Pack center on. Raise Core for a wider solid middle. Center mix is the soft apron. Brush Size widens the whole blend."
				: "Pack center off. Soft apron only: ground + path. Center mix is the apron mix.",
			SubTerrainPaintLayer.Base => "Ground writes the selected tmat as base and overlay, mix 0.",
			_ => "Wear writes overlay mix. Steep skips flats. Ctrl reduces. Alt-click locks Stay on sampled."
		};

		return body + IntentSuffix;
	}

	[EditorEvent.Frame]
	public void Refresh()
	{
		if ( _live is null || _modes is null || _listHost is null || _optionsHost is null )
			return;

		_tool.RefreshPaintChrome();

		if ( !_tool.PaintArmed )
			return;

		var terrain = _tool.PeekTerrain();
		int count = terrain?.Storage?.Materials?.Count ?? 0;
		if ( count != _listedCount )
			RebuildList();

		var settings = _tool.PaintSettings;
		if ( _sheetLayer != settings.Layer || _sheetSteep != settings.OnlyOnSlope )
			RebuildOptions();

		if ( _keepPairBox is not null )
			_keepPairBox.Value = settings.MixOnly;

		if ( _sampledBox is not null )
		{
			_sampledBox.Value = settings.LimitToSampledGround;
			_sampledBox.Enabled = settings.HasSampledGround;
			if ( !settings.HasSampledGround )
				_sampledBox.ToolTip = "Alt-click the ground to lock Stay on sampled.";
			else
				_sampledBox.ToolTip = "Locked to the Alt-clicked ground base. Uncheck to paint any base.";
		}

		if ( _packCenterBox is not null )
			_packCenterBox.Value = settings.PackCenter;

		if ( _limitRow is not null )
			_limitRow.Enabled = settings.HasSampledGround;

		RefreshCopy();
		_modes.Update();
		SyncPairPreview();

		if ( !_tool.StatusValid || terrain is null || !terrain.IsValid() )
			return;

		if ( !SubTerrainSplatUtil.TryTexel( terrain, _tool.StatusHitLocal, out var x, out var y ) )
			return;

		var mat = SubTerrainSplatUtil.Read( terrain, x, y );
		if ( mat.IsHole )
		{
			_live.Text = WriteLine() + " | hole";
			return;
		}

		var baseName = SubTerrainSplatUtil.MaterialName( terrain, mat.BaseTextureId );
		var overlayName = SubTerrainSplatUtil.MaterialName( terrain, mat.OverlayTextureId );
		float mix = mat.BlendFactor / 255f;
		string there = baseName == overlayName || mix < 0.02f
			? baseName
			: $"{baseName} + {overlayName} {mix:0.00}";

		string extra = "";
		if ( SubTerrainSplatUtil.TrySlopeDegrees( terrain, x, y, out var degrees ) )
		{
			int deg = (int)MathF.Round( degrees );
			bool steepGate = settings.Layer == SubTerrainPaintLayer.Overlay && settings.OnlyOnSlope;
			if ( steepGate )
				extra = degrees >= settings.SlopeDegrees ? $" | steep {deg}" : $" | skip {deg}";
		}

		if ( SubTerrainSplatUtil.HasSeam( terrain, x, y ) )
			extra += " | seam";

		_live.Text = $"{WriteLine()} | {there}{extra}";
	}
}

internal class SubTerrainPairPreview : Widget
{
	readonly SubTerrainTool _tool;
	const float ThumbSize = 24f;

	public SubTerrainPairPreview( SubTerrainTool tool, Widget parent ) : base( parent )
	{
		_tool = tool;
		FixedHeight = 36;
		MinimumHeight = 36;
		MaximumHeight = 36;
		ToolTip = "Pair this stroke will write.";
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		if ( !Visible || _tool.PaintSettings.Layer == SubTerrainPaintLayer.Base )
			return;

		Paint.Antialiasing = true;
		Paint.ClearPen();
		Paint.SetBrush( Color.White.WithAlpha( 0.04f ) );
		Paint.DrawRect( LocalRect );

		ResolvePair( out int left, out int right );
		float y = (Height - ThumbSize) * 0.5f;
		float gap = 8f;
		float total = ThumbSize * 2f + gap;
		float x0 = (Width - total) * 0.5f;

		DrawThumb( new Rect( x0, y, ThumbSize, ThumbSize ), left );
		DrawThumb( new Rect( x0 + ThumbSize + gap, y, ThumbSize, ThumbSize ), right );
	}

	void ResolvePair( out int left, out int right )
	{
		var settings = _tool.PaintSettings;
		var terrain = _tool.PeekTerrain();
		int selected = settings.MaterialIndex;

		if ( settings.Layer == SubTerrainPaintLayer.Path )
		{
			left = SubTerrainSplatUtil.FindPathGround( terrain, settings );
			right = selected;
			return;
		}

		if ( settings.Layer == SubTerrainPaintLayer.Overlay )
		{
			left = settings.HasSampledGround
				? settings.SampledGroundIndex
				: HoverBaseOr( terrain, selected );
			right = selected;
			return;
		}

		left = selected;
		right = selected;
	}

	int HoverBaseOr( Terrain terrain, int fallback )
	{
		if ( terrain is null || !_tool.StatusValid )
			return fallback;

		if ( !SubTerrainSplatUtil.TryTexel( terrain, _tool.StatusHitLocal, out var x, out var y ) )
			return fallback;

		var mat = SubTerrainSplatUtil.Read( terrain, x, y );
		if ( mat.IsHole )
			return fallback;

		return mat.BaseTextureId;
	}

	void DrawThumb( Rect rect, int index )
	{
		Paint.ClearPen();
		Paint.SetBrush( Theme.SurfaceBackground.WithAlpha( 0.35f ) );
		Paint.DrawRect( rect, 3 );

		var path = MaterialPath( index );
		var asset = string.IsNullOrWhiteSpace( path ) ? null : AssetSystem.FindByPath( path );
		if ( asset is null || asset.IsDeleted )
			return;

		var thumb = asset.GetAssetThumb( true );
		if ( thumb is not null )
			Paint.Draw( rect, thumb );
	}

	string MaterialPath( int index )
	{
		var mats = _tool.PeekTerrain()?.Storage?.Materials;
		if ( mats is null || index < 0 || index >= mats.Count )
			return null;

		return mats[index]?.ResourcePath;
	}

	[EditorEvent.Frame]
	public void TickPreview()
	{
		if ( !Visible || _tool.PaintSettings.Layer == SubTerrainPaintLayer.Base )
			return;

		Update();
	}
}

internal class SubTerrainPaintModeStrip : Widget
{
	readonly SubTerrainTool _tool;
	readonly SubTerrainBlendWidget _host;
	const int SlotCount = 3;
	int _hover = -1;

	public SubTerrainPaintModeStrip( SubTerrainTool tool, SubTerrainBlendWidget host ) : base( host )
	{
		_tool = tool;
		_host = host;
		FixedHeight = 28;
		MinimumHeight = 28;
		MaximumHeight = 28;
		Cursor = CursorShape.Finger;
		MouseTracking = true;
		ToolTip = "Ground is a solid tmat. Wear is overlay mix. Path packs a path tmat (optional Pack center) with a ground+path apron.";
	}

	protected override void OnMouseClick( MouseEvent e )
	{
		int slot = SlotAt( e.LocalPosition.x );
		if ( slot == 0 )
			_host.SelectGround();
		else if ( slot == 1 )
			_host.SelectWear();
		else
			_host.SelectPath();

		Update();
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		int next = SlotAt( e.LocalPosition.x );
		if ( next == _hover )
			return;

		_hover = next;
		Update();
	}

	protected override void OnMouseLeave()
	{
		_hover = -1;
		Update();
		base.OnMouseLeave();
	}

	int SlotAt( float x )
	{
		float w = Math.Max( Width, 1f );
		int slot = (int)(x / w * SlotCount);
		return Math.Clamp( slot, 0, SlotCount - 1 );
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		Paint.Antialiasing = true;
		Paint.ClearPen();
		Paint.SetBrush( Color.White.WithAlpha( 0.04f ) );
		Paint.DrawRect( LocalRect );

		var layer = _tool.PaintSettings.Layer;
		float slotW = Width / SlotCount;
		for ( int i = 0; i < SlotCount; i++ )
		{
			var rect = new Rect( i * slotW, 0, slotW, Height );
			bool selected = i switch
			{
				0 => layer == SubTerrainPaintLayer.Base,
				1 => layer == SubTerrainPaintLayer.Overlay,
				_ => layer == SubTerrainPaintLayer.Path
			};

			Paint.ClearPen();
			if ( selected )
				Paint.SetBrush( Theme.Primary.WithAlpha( 0.20f ) );
			else if ( _hover == i )
				Paint.SetBrush( Color.White.WithAlpha( 0.06f ) );
			else
				Paint.SetBrush( Color.Transparent );

			Paint.DrawRect( rect );

			if ( i > 0 )
			{
				Paint.ClearPen();
				Paint.SetBrush( Color.White.WithAlpha( 0.10f ) );
				Paint.DrawRect( new Rect( rect.Left, rect.Top + 5, 1, rect.Height - 10 ) );
			}

			string label = i switch
			{
				0 => "Ground",
				1 => "Wear",
				_ => "Path"
			};

			Paint.SetPen( selected ? Theme.Primary : Theme.TextControl );
			Paint.DrawText( rect, label, TextFlag.Center );
		}
	}
}

internal class SubTerrainWhereStrip : Widget
{
	readonly SubTerrainTool _tool;
	readonly SubTerrainBlendWidget _host;
	const int SlotCount = 2;
	int _hover = -1;

	public SubTerrainWhereStrip( SubTerrainTool tool, SubTerrainBlendWidget host ) : base( host )
	{
		_tool = tool;
		_host = host;
		FixedHeight = 28;
		MinimumHeight = 28;
		MaximumHeight = 28;
		Cursor = CursorShape.Finger;
		MouseTracking = true;
		ToolTip = "Steep skips ground that is too flat. All paints the whole brush, including shelves.";
	}

	protected override void OnMouseClick( MouseEvent e )
	{
		if ( SlotAt( e.LocalPosition.x ) == 0 )
			_host.SelectSteep();
		else
			_host.SelectAll();

		Update();
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		int next = SlotAt( e.LocalPosition.x );
		if ( next == _hover )
			return;

		_hover = next;
		ToolTip = next == 0
			? "Skip ground that is too flat. Hover a hill; the footer says steep or skip."
			: "Paint the whole brush, including shelves.";
		Update();
	}

	protected override void OnMouseLeave()
	{
		_hover = -1;
		ToolTip = "Steep skips ground that is too flat. All paints the whole brush, including shelves.";
		Update();
		base.OnMouseLeave();
	}

	int SlotAt( float x )
	{
		float w = Math.Max( Width, 1f );
		int slot = (int)(x / w * SlotCount);
		return Math.Clamp( slot, 0, SlotCount - 1 );
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		Paint.Antialiasing = true;
		Paint.ClearPen();
		Paint.SetBrush( Color.White.WithAlpha( 0.04f ) );
		Paint.DrawRect( LocalRect );

		bool steep = _tool.PaintSettings.OnlyOnSlope;
		float slotW = Width / SlotCount;
		for ( int i = 0; i < SlotCount; i++ )
		{
			var rect = new Rect( i * slotW, 0, slotW, Height );
			bool selected = i == 0 ? steep : !steep;

			Paint.ClearPen();
			if ( selected )
				Paint.SetBrush( Theme.Primary.WithAlpha( 0.20f ) );
			else if ( _hover == i )
				Paint.SetBrush( Color.White.WithAlpha( 0.06f ) );
			else
				Paint.SetBrush( Color.Transparent );

			Paint.DrawRect( rect );

			if ( i > 0 )
			{
				Paint.ClearPen();
				Paint.SetBrush( Color.White.WithAlpha( 0.10f ) );
				Paint.DrawRect( new Rect( rect.Left, rect.Top + 5, 1, rect.Height - 10 ) );
			}

			Paint.SetPen( selected ? Theme.Primary : Theme.TextControl );
			Paint.DrawText( rect, i == 0 ? "Steep" : "All", TextFlag.Center );
		}
	}

	[EditorEvent.Frame]
	public void TickSelect()
	{
		Update();
	}
}

internal class SubTerrainMaterialRow : Widget
{
	readonly SubTerrainTool _tool;
	readonly int _index;
	readonly string _name;
	readonly string _resourcePath;

	const float RowHeight = 42f;
	const float ThumbSize = 28f;

	public SubTerrainMaterialRow( SubTerrainTool tool, int index, string name, Widget parent ) : base( parent )
	{
		_tool = tool;
		_index = index;
		_name = name;
		_resourcePath = tool.PeekTerrain()?.Storage?.Materials is { Count: > 0 } mats && index >= 0 && index < mats.Count
			? mats[index]?.ResourcePath
			: null;
		FixedHeight = RowHeight;
		MinimumHeight = RowHeight;
		MaximumHeight = RowHeight;
		Cursor = CursorShape.Finger;
		ToolTip = name;
		MouseTracking = true;
	}

	protected override void OnMouseClick( MouseEvent e )
	{
		var settings = _tool.PaintSettings;
		bool path = settings.Layer == SubTerrainPaintLayer.Path;
		bool alt = Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt );

		if ( path && alt )
		{
			if ( _index == settings.MaterialIndex )
				return;

			settings.PathGroundIndex = _index;
			Update();
			return;
		}

		if ( path && _index == SubTerrainSplatUtil.FindPathGround( _tool.PeekTerrain(), settings ) )
		{
			settings.PathGroundIndex = _index;
			Update();
			return;
		}

		settings.MaterialIndex = _index;
		SubTerrainPaintTool.TrySetSplatChannel( _index );
		Update();
	}

	protected override void OnDoubleClick( MouseEvent e )
	{
		if ( string.IsNullOrWhiteSpace( _resourcePath ) )
			return;

		var asset = AssetSystem.FindByPath( _resourcePath );
		if ( asset is null || asset.IsDeleted )
			return;

		asset.OpenInEditor();
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		var settings = _tool.PaintSettings;
		bool selected = settings.MaterialIndex == _index;
		bool path = settings.Layer == SubTerrainPaintLayer.Path;
		bool pathGround = path && SubTerrainSplatUtil.FindPathGround( _tool.PeekTerrain(), settings ) == _index;

		Paint.Antialiasing = true;
		Paint.ClearPen();
		if ( selected )
			Paint.SetBrush( Theme.Primary.WithAlpha( 0.18f ) );
		else if ( IsUnderMouse )
			Paint.SetBrush( Color.White.WithAlpha( 0.06f ) );
		else
			Paint.SetBrush( Color.Transparent );

		Paint.DrawRect( LocalRect );

		var thumbRect = new Rect( 8, (RowHeight - ThumbSize) * 0.5f, ThumbSize, ThumbSize );
		Paint.ClearPen();
		Paint.SetBrush( Theme.SurfaceBackground.WithAlpha( 0.35f ) );
		Paint.DrawRect( thumbRect, 3 );

		var asset = string.IsNullOrWhiteSpace( _resourcePath ) ? null : AssetSystem.FindByPath( _resourcePath );
		if ( asset is not null && !asset.IsDeleted )
		{
			var thumb = asset.GetAssetThumb( true );
			if ( thumb is not null )
				Paint.Draw( thumbRect, thumb );
		}

		float textLeft = thumbRect.Right + 8f;
		Paint.SetPen( selected ? Color.White : Theme.TextControl );
		Paint.DrawText( new Rect( textLeft, 0, Math.Max( 40f, Width - textLeft - 56f ), Height ), _name, TextFlag.LeftCenter );

		string role = null;
		if ( pathGround )
			role = "ground";
		else if ( path && selected )
			role = "path";
		else if ( !path && selected && settings.Layer == SubTerrainPaintLayer.Overlay )
			role = settings.MixCap.ToString( "0.00" );

		if ( role is not null )
		{
			Paint.SetPen( Color.White.WithAlpha( selected ? 0.70f : 0.45f ) );
			Paint.DrawText( new Rect( Width - 56, 0, 44, Height ), role, TextFlag.RightCenter );
		}
	}

	[EditorEvent.Frame]
	public void TickSelect()
	{
		bool path = _tool.PaintSettings.Layer == SubTerrainPaintLayer.Path;
		ToolTip = path
			? "Click the packed tmat. Alt-click the ground under it. Double-click opens the asset."
			: _name + " | Double-click opens the asset.";
		Update();
	}
}
subzerostudios.subterrain-tool / Code/TerrainSculptNet.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;

namespace SubZero.SubTerrain;

/// <summary>
/// Optional host-authoritative play-sculpt net. Attach to a NetworkMode.Object
/// GameObject. Host writes height; peers apply the same op locally.
/// </summary>
[Title( "Terrain Sculpt Net" )]
[Category( "SubTerrain" )]
[Icon( "share" )]
public sealed class TerrainSculptNet : Component
{
	public bool Submit( TerrainEditRequest request )
	{
		if ( request.Seed == 0 )
			request.Seed = Game.Random.Int( 1, 1_000_000_000 );

		if ( !Networking.IsActive || Networking.IsHost )
		{
			ApplyLocal( request );
			if ( Networking.IsActive )
				BroadcastStamp( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );
			return true;
		}

		RequestHost( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );
		return true;
	}

	[Rpc.Host]
	public void RequestHost( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )
	{
		var request = Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ );
		ApplyLocal( request );
		BroadcastStamp( kind, pos, radius, depth, seed, hasTargetZ, targetZ );
	}

	[Rpc.Broadcast]
	public void BroadcastStamp( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )
	{
		if ( Networking.IsHost )
			return;

		ApplyLocal( Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ ) );
	}

	static TerrainEditRequest Make( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )
	{
		return new TerrainEditRequest
		{
			Kind = (TerrainEditKind)kind,
			WorldPos = pos,
			Radius = radius,
			Depth = depth,
			Seed = seed,
			HasTargetZ = hasTargetZ,
			TargetZ = targetZ
		};
	}

	void ApplyLocal( TerrainEditRequest request )
	{
		TerrainSculpt.TryApply( Scene, request );
	}
}
subzerostudios.subterrain-tool / TerrainBrushSettings.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using Sandbox;

namespace SubZero.SubTerrain;

public sealed class CraterBrushSettings
{
	[Property, Range( 16f, 200f )]
	public float MaxDepth { get; set; } = 96f;

	[Property, Title( "Depth Scale" ), Range( 0.1f, 1.5f )]
	public float DepthScale { get; set; } = 0.5f;

	[Property, Range( 0f, 32f )]
	public float MaxRaise { get; set; } = 12f;

	[Property, Range( 0.4f, 0.95f )]
	public float BowlRadius { get; set; } = 0.75f;

	[Property, Range( 0f, 0.7f )]
	public float Irregularity { get; set; } = 0.35f;

	[Property, Range( 0f, 0.5f )]
	public float Stretch { get; set; } = 0.25f;

	[Property, Range( 0f, 1f )]
	public float Blend { get; set; } = 0.65f;
}

public sealed class DigBrushSettings
{
	[Property, Range( 16f, 120f )]
	public float MaxDepth { get; set; } = 48f;

	[Property, Title( "Depth Scale" ), Range( 0.1f, 1.5f )]
	public float DepthScale { get; set; } = 0.85f;

	[Property, Range( 0f, 40f )]
	public float MaxRaise { get; set; } = 18f;

	[Property, Range( 0.7f, 0.98f )]
	public float BowlRadius { get; set; } = 0.9f;

	[Property, Range( 0f, 0.4f )]
	public float Irregularity { get; set; } = 0.12f;

	[Property, Range( 0f, 0.35f )]
	public float Stretch { get; set; } = 0.08f;

	[Property, Range( 0f, 1f )]
	public float Blend { get; set; } = 0.35f;
}

public sealed class FlattenBrushSettings
{
	[Property, Range( 0.05f, 1f )]
	public float Strength { get; set; } = 0.65f;

	[Property, Range( 4f, 64f )]
	public float MaxDelta { get; set; } = 24f;
}
subzerostudios.subterrain-tool / Editor/Tools/BridgeTool.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;
using Editor.TerrainEditor;

namespace Editor.SubTerrain;

/// <summary>
/// Bridge tool. Press on the first point and release on the second to raise a land bridge
/// spanning the gap. The deck grades from the start ground height to the end ground height,
/// and the whole span is written once on release rather than painted stroke by stroke.
/// </summary>
[Title( "Bridge" )]
[Description( "Press start, release on end. Raises a land bridge between the two heights." )]
[Icon( "linear_scale" )]
[Alias( "subterrain_bridge" )]
[Group( "1" )]
[Order( 10 )]
public class SubBridgeTool : SubTerrainBaseBrushTool
{
	Vector3 _startLocal;
	Vector3 _endLocal;
	Vector2 _endUV;
	float _halfWidthTexels;

	public SubBridgeTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Bridge;
	}

	protected override void OnStrokeStart( Terrain terrain, Vector3 hitPosition )
	{
		_startLocal = hitPosition;
		_endLocal = hitPosition;

		SlopeStartUV = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize;
		SlopeStartHeight01 = hitPosition.z / terrain.Storage.TerrainHeight;
	}

	protected override void OnPaint( Terrain terrain, SubTerrainPaintParameters paint )
	{
		_endLocal = paint.HitPosition;
	}

	protected override void OnPaintEnded( Terrain terrain )
	{
		BuildBridge( terrain );
		base.OnPaintEnded( terrain );
	}

	void BuildBridge( Terrain terrain )
	{
		var storage = terrain.Storage;
		var settings = _parent.BrushSettings;

		var startUV = new Vector2( _startLocal.x, _startLocal.y ) / storage.TerrainSize;
		_endUV = new Vector2( _endLocal.x, _endLocal.y ) / storage.TerrainSize;

		_halfWidthTexels = Math.Max( settings.BridgeWidth * 0.5f / storage.TerrainSize * storage.Resolution, 1.0f );

		var spanTexels = (_endUV - startUV) * storage.Resolution;
		if ( spanTexels.Length < 1.0f )
			return;

		int size = (int)Math.Ceiling( Math.Max( Math.Abs( spanTexels.x ), Math.Abs( spanTexels.y ) ) + _halfWidthTexels * 2.0f ) + 2;
		size = Math.Clamp( size, 1, storage.Resolution );

		SlopeStartUV = startUV;
		SlopeStartHeight01 = _startLocal.z / storage.TerrainHeight;

		var paint = new SubTerrainPaintParameters
		{
			HitPosition = _endLocal,
			HitUV = (startUV + _endUV) * 0.5f,
			FlattenHeight = _endLocal.z / storage.TerrainHeight,
			BrushSettings = settings
		};

		var brushTexture = _parent.SelectedBrushTexture;
		if ( brushTexture is null )
			return;

		DispatchSculpt( terrain, paint, brushTexture, size, 1.0f );
	}

	protected override void ApplyExtraAttributes( ComputeShader cs, SubTerrainPaintParameters paint )
	{
		var settings = paint.BrushSettings;

		cs.Attributes.Set( "SlopeEndUV", _endUV );
		cs.Attributes.Set( "SlopeEndHeight", paint.FlattenHeight );
		cs.Attributes.Set( "BridgeHalfWidth", _halfWidthTexels );
		cs.Attributes.Set( "BridgeDeckRatio", 1.0f - settings.BridgeEdgeFalloff );
		cs.Attributes.Set( "BridgeRaiseOnly", settings.BridgeRaiseOnly ? 1 : 0 );
	}

	protected override void DrawToolOverlay( Terrain terrain, Vector3 worldCenter )
	{
		if ( !_dragging )
			return;

		var startWorld = terrain.WorldTransform.PointToWorld( _startLocal );
		var span = worldCenter - startWorld;
		var flat = new Vector3( span.x, span.y, 0.0f );

		Gizmo.Draw.Color = Color.FromBytes( 250, 180, 120 );
		Gizmo.Draw.Line( startWorld, worldCenter );

		if ( flat.Length < 0.001f )
			return;

		var halfWidth = _parent.BrushSettings.BridgeWidth * 0.5f;
		var side = new Vector3( -flat.y, flat.x, 0.0f ) / flat.Length * halfWidth;

		Gizmo.Draw.Line( startWorld + side, worldCenter + side );
		Gizmo.Draw.Line( startWorld - side, worldCenter - side );
		Gizmo.Draw.Line( startWorld - side, startWorld + side );
		Gizmo.Draw.Line( worldCenter - side, worldCenter + side );
	}
}
subzerostudios.subterrain-tool / Editor/Tools/SetHeightTool.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using Sandbox;

namespace Editor.SubTerrain;

/// <summary>
/// Flatten to Target Height. Alt-click the ground to load that Z.
/// </summary>
[Title( "Set Height" )]
[Icon( "straighten" )]
[Alias( "subterrain_set_height" )]
[Group( "1" )]
[Order( 7 )]
public class SubSetHeightTool : SubTerrainBaseBrushTool
{
	public SubSetHeightTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Flatten;
		AllowHeightSample = true;
	}

	protected override void OnPaint( Terrain terrain, SubTerrainPaintParameters paint )
	{
		paint.FlattenHeight = paint.BrushSettings.TargetHeight / terrain.Storage.TerrainHeight;
		base.OnPaint( terrain, paint );
	}
}
subzerostudios.subterrain-tool / TerrainSculpt.cs
Game library
// 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}" );
	}
}
subzerostudios.subterrain-tool / Code/TerrainBrushSettings.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using Sandbox;

namespace SubZero.SubTerrain;

public sealed class CraterBrushSettings
{
	[Property, Range( 16f, 200f )]
	public float MaxDepth { get; set; } = 96f;

	[Property, Title( "Depth Scale" ), Range( 0.1f, 1.5f )]
	public float DepthScale { get; set; } = 0.5f;

	[Property, Range( 0f, 32f )]
	public float MaxRaise { get; set; } = 12f;

	[Property, Range( 0.4f, 0.95f )]
	public float BowlRadius { get; set; } = 0.75f;

	[Property, Range( 0f, 0.7f )]
	public float Irregularity { get; set; } = 0.35f;

	[Property, Range( 0f, 0.5f )]
	public float Stretch { get; set; } = 0.25f;

	[Property, Range( 0f, 1f )]
	public float Blend { get; set; } = 0.65f;
}

public sealed class DigBrushSettings
{
	[Property, Range( 16f, 120f )]
	public float MaxDepth { get; set; } = 48f;

	[Property, Title( "Depth Scale" ), Range( 0.1f, 1.5f )]
	public float DepthScale { get; set; } = 0.85f;

	[Property, Range( 0f, 40f )]
	public float MaxRaise { get; set; } = 18f;

	[Property, Range( 0.7f, 0.98f )]
	public float BowlRadius { get; set; } = 0.9f;

	[Property, Range( 0f, 0.4f )]
	public float Irregularity { get; set; } = 0.12f;

	[Property, Range( 0f, 0.35f )]
	public float Stretch { get; set; } = 0.08f;

	[Property, Range( 0f, 1f )]
	public float Blend { get; set; } = 0.35f;
}

public sealed class FlattenBrushSettings
{
	[Property, Range( 0.05f, 1f )]
	public float Strength { get; set; } = 0.65f;

	[Property, Range( 4f, 64f )]
	public float MaxDelta { get; set; } = 24f;
}
subzerostudios.subterrain-tool / Editor/SubTerrainPaintSettings.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

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

namespace Editor.SubTerrain;

/// <summary>
/// Paint Texture settings. Ground is the default mode. UI labels live on the [Title]
/// attributes; OnlyOnSlope has no inspector title (Steep | All owns it).
/// </summary>
public class SubTerrainPaintSettings
{
	public const float WearMixMax = 0.40f;
	public const float PathMixMax = 0.36f;
	public const float DitherWarn = 0.36f;
	public const float PackCoreMin = 0.15f;
	public const float PackCoreMax = 0.95f;
	public const string LibraryVersion = "1.4.15";

	public SubTerrainPaintLayer Layer { get; set; } = SubTerrainPaintLayer.Base;

	public int MaterialIndex { get; set; }

	public float MixCap { get; set; } = 0.28f;

	/// <summary>Wear only. Move mix on the pair that is already there. The selected tmat is not written.</summary>
	[Property, Title( "Keep pair" ), WideMode]
	public bool MixOnly { get; set; }

	/// <summary>Alt-click the ground to stay on. Wear will not paint other bases.</summary>
	[Property, Title( "Stay on sampled" ), WideMode]
	public bool LimitToSampledGround { get; set; }

	/// <summary>Path only. Hard brush center packs the path tmat as base.</summary>
	[Property, Title( "Pack center" ), Description( "On: solid path core along the stroke, soft apron outside. Off: soft ground + path apron only." ), WideMode]
	public bool PackCenter { get; set; } = true;

	/// <summary>Path + Pack center. Higher = wider solid middle. Outer ring stays soft blend.</summary>
	[Property, Title( "Core" ), Description( "How much of the brush packs solid. Raise for a wider solid middle. Outer ring stays Center mix apron." ), Range( PackCoreMin, PackCoreMax ), Step( 0.01f ), WideMode]
	public float PackCore { get; set; } = 0.55f;

	[Property, Title( "How worn" ), Description( "Overlay mix. Default 0.28. Soft chew to 0.36; hard stop 0.40. Above 0.36 can dither." ), Range( 0.05f, WearMixMax ), Step( 0.01f ), WideMode]
	public float HowWorn
	{
		get => MixCap;
		set => MixCap = value;
	}

	[Property, Title( "Center mix" ), Description( "Soft apron overlay mix outside the packed core. Raise with brush Size for a wider blend." ), Range( 0.05f, PathMixMax ), Step( 0.01f ), WideMode]
	public float CenterMix
	{
		get => MixCap;
		set => MixCap = value;
	}

	public bool OnlyOnSlope { get; set; } = true;

	[Property, Title( "Steeper than" ), Description( "How steep the ground must be. 20 is a hill face. Lower it to catch gentler banks." ), Range( 1f, 60f ), Step( 1f ), WideMode]
	public float SlopeDegrees { get; set; } = 20f;

	public int SampledGroundIndex { get; set; } = -1;

	public int PathGroundIndex { get; set; } = -1;

	public bool HasSampledGround => SampledGroundIndex >= 0;

	public bool HasPathGround => PathGroundIndex >= 0;

	public bool Dithers => MixCap > DitherWarn;

	/// <summary>Stamp value above which Pack center writes solid path. Higher Core = lower threshold.</summary>
	public float PackThreshold
	{
		get
		{
			float core = Math.Clamp( PackCore, PackCoreMin, PackCoreMax );
			return Math.Clamp( 0.82f - core * 0.64f, 0.15f, 0.85f );
		}
	}

	public static float MixMaxFor( SubTerrainPaintLayer layer )
	{
		return layer == SubTerrainPaintLayer.Path ? PathMixMax : WearMixMax;
	}
}

/// <summary>
/// Per Ground / Wear / Path mix and gate, for the editor session.
/// </summary>
internal static class SubTerrainPaintStore
{
	struct LayerState
	{
		public float MixCap;
		public bool OnlyOnSlope;
		public float SlopeDegrees;
		public bool MixOnly;
		public bool LimitToSampledGround;
		public bool PackCenter;
		public float PackCore;
	}

	static readonly Dictionary<SubTerrainPaintLayer, LayerState> ByLayer = new();

	public static void Capture( SubTerrainPaintSettings settings )
	{
		if ( settings is null )
			return;

		ByLayer[settings.Layer] = Read( settings );
	}

	public static void Apply( SubTerrainPaintSettings settings, SubTerrainPaintLayer layer )
	{
		if ( settings is null )
			return;

		if ( !ByLayer.TryGetValue( layer, out var state ) )
			state = Defaults( layer );

		settings.Layer = layer;
		settings.MixCap = Math.Clamp( state.MixCap, 0.05f, SubTerrainPaintSettings.MixMaxFor( layer ) );
		settings.OnlyOnSlope = state.OnlyOnSlope;
		settings.SlopeDegrees = state.SlopeDegrees;
		settings.MixOnly = state.MixOnly;
		settings.LimitToSampledGround = state.LimitToSampledGround;
		settings.PackCenter = state.PackCenter;
		settings.PackCore = Math.Clamp( state.PackCore, SubTerrainPaintSettings.PackCoreMin, SubTerrainPaintSettings.PackCoreMax );
	}

	static LayerState Read( SubTerrainPaintSettings settings )
	{
		return new LayerState
		{
			MixCap = settings.MixCap,
			OnlyOnSlope = settings.OnlyOnSlope,
			SlopeDegrees = settings.SlopeDegrees,
			MixOnly = settings.MixOnly,
			LimitToSampledGround = settings.LimitToSampledGround,
			PackCenter = settings.PackCenter,
			PackCore = settings.PackCore
		};
	}

	static LayerState Defaults( SubTerrainPaintLayer layer )
	{
		if ( layer == SubTerrainPaintLayer.Path )
		{
			return new LayerState
			{
				MixCap = 0.36f,
				OnlyOnSlope = false,
				SlopeDegrees = 20f,
				PackCenter = true,
				PackCore = 0.55f
			};
		}

		if ( layer == SubTerrainPaintLayer.Overlay )
		{
			return new LayerState
			{
				MixCap = 0.28f,
				OnlyOnSlope = true,
				SlopeDegrees = 20f,
				PackCenter = true,
				PackCore = 0.55f
			};
		}

		return new LayerState
		{
			MixCap = 0.28f,
			OnlyOnSlope = false,
			SlopeDegrees = 20f,
			PackCenter = true,
			PackCore = 0.55f
		};
	}
}
subzerostudios.subterrain-tool / TerrainSculptNet.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;

namespace SubZero.SubTerrain;

/// <summary>
/// Optional host-authoritative play-sculpt net. Attach to a NetworkMode.Object
/// GameObject. Host writes height; peers apply the same op locally.
/// </summary>
[Title( "Terrain Sculpt Net" )]
[Category( "SubTerrain" )]
[Icon( "share" )]
public sealed class TerrainSculptNet : Component
{
	public bool Submit( TerrainEditRequest request )
	{
		if ( request.Seed == 0 )
			request.Seed = Game.Random.Int( 1, 1_000_000_000 );

		if ( !Networking.IsActive || Networking.IsHost )
		{
			ApplyLocal( request );
			if ( Networking.IsActive )
				BroadcastStamp( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );
			return true;
		}

		RequestHost( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );
		return true;
	}

	[Rpc.Host]
	public void RequestHost( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )
	{
		var request = Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ );
		ApplyLocal( request );
		BroadcastStamp( kind, pos, radius, depth, seed, hasTargetZ, targetZ );
	}

	[Rpc.Broadcast]
	public void BroadcastStamp( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )
	{
		if ( Networking.IsHost )
			return;

		ApplyLocal( Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ ) );
	}

	static TerrainEditRequest Make( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )
	{
		return new TerrainEditRequest
		{
			Kind = (TerrainEditKind)kind,
			WorldPos = pos,
			Radius = radius,
			Depth = depth,
			Seed = seed,
			HasTargetZ = hasTargetZ,
			TargetZ = targetZ
		};
	}

	void ApplyLocal( TerrainEditRequest request )
	{
		TerrainSculpt.TryApply( Scene, request );
	}
}
subzerostudios.subterrain-tool / TerrainDebrisFx.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

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

namespace SubZero.SubTerrain;

/// <summary>
/// Dirt spray and physics clods for crater / dig stamps. Listens to
/// TerrainSculpt.OnApplied after GPU and collision so debris lands in the bowl.
/// Prefabs use engine meshes (models/dev/box.vmdl).
/// </summary>
[Title( "Terrain Debris FX" )]
[Category( "SubTerrain" )]
[Icon( "grain" )]
public sealed class TerrainDebrisFx : Component
{
	const string DefaultClodPath = "prefabs/subterrain/dirt_clod.prefab";
	const string DefaultSprayPath = "prefabs/subterrain/dirt_spray.prefab";
	const FindMode RigidbodyFind = FindMode.Enabled | FindMode.InSelf | FindMode.InDescendants;

	[Property, Group( "Source" )]
	public TerrainSculpt Sculpt { get; set; }

	[Property, Group( "Prefabs" )]
	public PrefabFile ClodPrefab { get; set; }

	[Property, Group( "Prefabs" )]
	public PrefabFile SprayPrefab { get; set; }

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

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

	[Property, Group( "Clods" ), Range( 0, 48 )]
	public int MinClods { get; set; } = 8;

	[Property, Group( "Clods" ), Range( 1, 64 )]
	public int MaxClods { get; set; } = 24;

	[Property, Group( "Clods" ), Range( 0f, 0.5f )]
	public float ClodsPerRadius { get; set; } = 0.12f;

	[Property, Group( "Clods" ), Range( 0, 128 )]
	public int MaxAlive { get; set; } = 64;

	[Property, Group( "Launch" ), Range( 0f, 4000f )]
	public float Impulse { get; set; } = 640f;

	[Property, Group( "Launch" ), Range( 0f, 3f )]
	public float UpBias { get; set; } = 0.45f;

	[Property, Group( "Launch" ), Range( 0.5f, 4f )]
	public float Outward { get; set; } = 2.2f;

	[Property, Group( "Launch" ), Range( 0.4f, 1.4f )]
	public float RimMin { get; set; } = 0.78f;

	[Property, Group( "Launch" ), Range( 0.5f, 1.6f )]
	public float RimMax { get; set; } = 1.12f;

	[Property, Group( "Launch" ), Range( 0f, 40f )]
	public float Spin { get; set; } = 14f;

	[Property, Group( "Launch" ), Range( 0f, 48f )]
	public float SpawnLift { get; set; } = 20f;

	[Property, Group( "Launch" ), Range( 0.05f, 1.5f )]
	public float ScaleMin { get; set; } = 0.7f;

	[Property, Group( "Launch" ), Range( 0.05f, 2.5f )]
	public float ScaleMax { get; set; } = 1.35f;

	[Property, Group( "Dig" ), Range( 0.1f, 1f )]
	public float DigImpulseScale { get; set; } = 0.5f;

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

	readonly List<GameObject> _alive = new();

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

		Sculpt ??= Components.Get<TerrainSculpt>( FindMode.EnabledInSelf );
		ClodPrefab ??= ResourceLibrary.Get<PrefabFile>( DefaultClodPath );
		SprayPrefab ??= ResourceLibrary.Get<PrefabFile>( DefaultSprayPath );

		if ( Sculpt == null || !Sculpt.IsValid() )
		{
			Log.Warning( $"[Debris] {GameObject.Name}: no TerrainSculpt on this object" );
			return;
		}

		Sculpt.OnApplied += OnTerrainApplied;
		Sculpt.OnReset += ClearDebris;
	}

	protected override void OnDisabled()
	{
		Unhook();
		ClearDebris();
	}

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

	void Unhook()
	{
		if ( Sculpt == null || !Sculpt.IsValid() )
			return;

		Sculpt.OnApplied -= OnTerrainApplied;
		Sculpt.OnReset -= ClearDebris;
	}

	void OnTerrainApplied( TerrainEditResult result )
	{
		if ( result.Kind == TerrainEditKind.Flatten )
			return;

		if ( result.Kind == TerrainEditKind.Crater && !Craters )
			return;

		if ( result.Kind == TerrainEditKind.Dig && !Digs )
			return;

		SpawnSpray( result );
		SpawnClods( result );
	}

	void SpawnSpray( TerrainEditResult result )
	{
		if ( SprayPrefab == null )
			return;

		var aimUp = Rotation.LookAt( Vector3.Up, Vector3.Forward );
		var spray = GameObject.Clone( SprayPrefab, new Transform( result.Origin, aimUp ) );
		if ( !spray.IsValid() )
			return;

		Track( spray );
	}

	void SpawnClods( TerrainEditResult result )
	{
		if ( ClodPrefab == null )
			return;

		var count = (int)Math.Clamp( result.Radius * ClodsPerRadius, MinClods, MaxClods );
		if ( result.Kind == TerrainEditKind.Dig )
			count = Math.Max( 3, count / 2 );

		var impulse = Impulse;
		var up = UpBias;
		if ( result.Kind == TerrainEditKind.Dig )
		{
			impulse *= DigImpulseScale;
			up *= 0.65f;
		}

		for ( var i = 0; i < count; i++ )
			SpawnClod( result, impulse, up, i, count );

		Dbg( $"spawned {count} clods kind={result.Kind} r={result.Radius:0.#}" );
	}

	void SpawnClod( TerrainEditResult result, float impulse, float up, int index, int count )
	{
		var yaw = (360f * index / Math.Max( count, 1 )) + Random.Shared.Float( -14f, 14f );
		var outward = Rotation.FromYaw( yaw ).Forward;
		var rimMin = result.Kind == TerrainEditKind.Dig
			? Math.Clamp( RimMin * 0.7f, 0.4f, 1.2f )
			: Math.Clamp( RimMin, 0.4f, 1.4f );
		var rimMax = Math.Max( rimMin + 0.05f, RimMax );
		var radial = result.Radius * Random.Shared.Float( rimMin, rimMax );
		var pos = result.Origin + outward * radial + Vector3.Up * SpawnLift;
		var scale = Random.Shared.Float( ScaleMin, ScaleMax );
		var rot = Rotation.From( new Angles(
			Random.Shared.Float( 0f, 360f ),
			Random.Shared.Float( 0f, 360f ),
			Random.Shared.Float( 0f, 360f )
		) );

		var clod = GameObject.Clone( ClodPrefab, new Transform( pos, rot ) );
		if ( !clod.IsValid() )
			return;

		clod.WorldScale *= scale;

		Track( clod );

		var body = clod.Components.Get<Rigidbody>( RigidbodyFind );
		if ( !body.IsValid() )
			return;

		var dir = (outward * Outward + Vector3.Up * up).Normal;
		var speed = impulse * Random.Shared.Float( 0.85f, 1.25f );
		body.Sleeping = false;
		body.Velocity = dir * speed;
		body.AngularVelocity = new Vector3(
			Random.Shared.Float( -1f, 1f ),
			Random.Shared.Float( -1f, 1f ),
			Random.Shared.Float( -1f, 1f )
		) * Spin;
	}

	void Track( GameObject go )
	{
		Prune();
		_alive.Add( go );

		while ( MaxAlive > 0 && _alive.Count > MaxAlive )
		{
			var oldest = _alive[0];
			_alive.RemoveAt( 0 );
			if ( oldest.IsValid() )
				oldest.Destroy();
		}
	}

	void ClearDebris()
	{
		for ( var i = _alive.Count - 1; i >= 0; i-- )
		{
			if ( _alive[i].IsValid() )
				_alive[i].Destroy();
		}

		_alive.Clear();
	}

	void Prune()
	{
		for ( var i = _alive.Count - 1; i >= 0; i-- )
		{
			if ( !_alive[i].IsValid() )
				_alive.RemoveAt( i );
		}
	}

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

		Log.Info( $"[Debris] {message}" );
	}
}
subzerostudios.subterrain-tool / Editor/SubTerrainBaseBrushTool.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;
using Editor.TerrainEditor;

namespace Editor.SubTerrain;

public struct SubTerrainPaintParameters
{
	public Vector3 HitPosition { get; set; }
	public Vector2 HitUV { get; set; }
	public float FlattenHeight { get; set; }
	public SubTerrainBrushSettings BrushSettings { get; set; }
}

/// <summary>
/// Shared stroke logic for SubTerrain sculpt tools.
/// </summary>
public abstract class SubTerrainBaseBrushTool : EditorTool
{
	protected SubTerrainTool _parent;
	protected bool _dragging;
	protected RectInt _dirtyRegion;

	protected SubTerrainSculptMode Mode { get; set; }
	protected Vector2 SlopeStartUV;
	protected float SlopeStartHeight01;
	protected bool AllowBrushInvert { get; set; }
	protected bool AllowHeightSample { get; set; }
	protected Plane StrokePlane;
	bool _samplingHeight;

	GpuBuffer<BrushData> _engineBrushBuffer;

	Vector3 _lastHitWorldPos;
	Vector3 _lastPaintLocal;
	Transform _lastHitTx;
	Vector2? _cursorLockPosition;
	float _raiseStampAccum;

	public SubTerrainBaseBrushTool( SubTerrainTool parent )
	{
		_parent = parent;
	}

	public override void OnEnabled()
	{
		AllowGameObjectSelection = false;
		_parent.SetBrushMode( GetType().Name );
	}

	public override void OnDisabled()
	{
		_parent.DetachSidebarForReuse();
	}

	public virtual bool GetHitPosition( Terrain terrain, out Vector3 position )
	{
		return terrain.RayIntersects( Gizmo.CurrentRay, Gizmo.RayDepth, out position );
	}

	public override void OnUpdate()
	{
		var terrain = GetSelectedComponent<Terrain>();
		if ( !terrain.IsValid() )
		{
			_parent.ClearStatus();
			return;
		}

		if ( !GetHitPosition( terrain, out var hitPosition ) )
		{
			_parent.ClearStatus();
			return;
		}

		var tx = terrain.WorldTransform;
		_parent.UpdateStatus( terrain, hitPosition );

		if ( Application.MouseButtons.HasFlag( MouseButtons.Middle ) )
		{
			_cursorLockPosition ??= Application.UnscaledCursorPosition;
			var d = Application.UnscaledCursorPosition - _cursorLockPosition.Value;
			var brush = _parent.BrushSettings;

			if ( Gizmo.IsShiftPressed )
			{
				brush.Size = SubTerrainBrushSize.Clamp( (int)(brush.Size + d.x * 0.25f) );
				brush.Opacity = Math.Clamp( brush.Opacity - d.y * 0.002f, 0f, 1f );
				_parent.SyncBrushSizeWidget();
			}
			else if ( Gizmo.IsCtrlPressed && !brush.RandomRotation )
			{
				brush.Rotation = ((brush.Rotation + d.x * 0.5f) % 360f + 360f) % 360f;
				_parent.SyncBrushRotationRows();
			}

			Application.UnscaledCursorPosition = _cursorLockPosition.Value;
			SceneOverlay.Parent.Cursor = CursorShape.Blank;

			DrawBrushAdjustText();
			_parent.DrawBrushPreview( new Transform( _lastHitWorldPos, _lastHitTx.Rotation ), terrain );
			return;
		}

		if ( _cursorLockPosition.HasValue )
		{
			SceneOverlay.Parent.Cursor = CursorShape.None;
			_cursorLockPosition = null;
		}

		_lastHitWorldPos = tx.PointToWorld( hitPosition );
		_lastHitTx = tx;

		if ( AllowHeightSample && Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt ) )
		{
			if ( Gizmo.IsLeftMouseDown )
			{
				if ( !_samplingHeight )
				{
					_samplingHeight = true;
					_parent.BrushSettings.TargetHeight = hitPosition.z;
				}
			}
			else
			{
				_samplingHeight = false;
			}

			_parent.DrawBrushPreview( new Transform( _lastHitWorldPos, tx.Rotation ), terrain );
			DrawToolOverlay( terrain, _lastHitWorldPos );
			return;
		}

		_samplingHeight = false;

		if ( Gizmo.IsLeftMouseDown )
		{
			bool moved = !Application.CursorDelta.IsNearZeroLength;
			bool shouldSculpt = !_dragging || moved;

			if ( !_dragging )
			{
				if ( _parent.BrushSettings.RandomRotation )
					_parent.BrushSettings.Rotation = Random.Shared.NextSingle() * 360f;

				StrokePlane = new Plane( _lastHitWorldPos, tx.Rotation.Up );
				_dragging = true;
				_raiseStampAccum = 0f;

				var uv = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize;
				var x = (int)Math.Floor( terrain.Storage.Resolution * uv.x );
				var y = (int)Math.Floor( terrain.Storage.Resolution * uv.y );
				_dirtyRegion = new( new Vector2Int( x, y ) );
				_lastPaintLocal = hitPosition;

				OnStrokeStart( terrain, hitPosition );
				OnPaint( terrain, MakePaintParams( hitPosition, terrain ) );
			}
			else if ( Mode == SubTerrainSculptMode.RaiseLower )
			{
				if ( moved )
				{
					StrokeSculpt( terrain, hitPosition );
					_raiseStampAccum = 0f;
				}
				else
				{
					// Hold in place: keep raising/lowering at Speed stamps per second.
					float t = Math.Clamp( (_parent.BrushSettings.Speed - 1) / 99f, 0f, 1f );
					float stampsPerSec = 2f + t * 58f;
					_raiseStampAccum += Math.Clamp( RealTime.Delta, 0f, 0.1f ) * stampsPerSec;
					while ( _raiseStampAccum >= 1f )
					{
						_raiseStampAccum -= 1f;
						OnPaint( terrain, MakePaintParams( hitPosition, terrain ) );
						_lastPaintLocal = hitPosition;
					}
				}
			}
			else if ( shouldSculpt )
			{
				StrokeSculpt( terrain, hitPosition );
			}
		}
		else if ( _dragging )
		{
			_dragging = false;
			_raiseStampAccum = 0f;
			OnPaintEnded( terrain );
		}

		_parent.DrawBrushPreview( new Transform( _lastHitWorldPos, tx.Rotation ), terrain );
		DrawToolOverlay( terrain, _lastHitWorldPos );
	}

	SubTerrainPaintParameters MakePaintParams( Vector3 hitPosition, Terrain terrain )
	{
		return new SubTerrainPaintParameters
		{
			HitPosition = hitPosition,
			HitUV = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize,
			FlattenHeight = hitPosition.z / terrain.Storage.TerrainHeight,
			BrushSettings = _parent.BrushSettings
		};
	}

	/// <summary>
	/// World-space stamp fill so fast / zoomed-out drags do not leave dashed strokes.
	/// </summary>
	void StrokeSculpt( Terrain terrain, Vector3 hitLocal )
	{
		var from = _lastPaintLocal;
		var to = hitLocal;
		float dist = (to - from).Length;
		float step = Math.Max( _parent.BrushSettings.Size * 0.2f, 8f );

		if ( dist < 0.001f )
			return;

		if ( dist <= step )
		{
			OnPaint( terrain, MakePaintParams( to, terrain ) );
			_lastPaintLocal = to;
			return;
		}

		const int maxStamps = 64;
		int stamps = (int)Math.Ceiling( dist / step );
		if ( stamps > maxStamps )
		{
			stamps = maxStamps;
			step = dist / stamps;
		}

		for ( int i = 1; i <= stamps; i++ )
		{
			float t = i / (float)stamps;
			OnPaint( terrain, MakePaintParams( Vector3.Lerp( from, to, t ), terrain ) );
		}

		_lastPaintLocal = to;
	}

	void DrawBrushAdjustText()
	{
		var textScope = new TextRendering.Scope
		{
			TextColor = Color.White,
			FontSize = 16 * Gizmo.Settings.GizmoScale * Application.DpiScale,
			FontName = "Roboto Mono",
			FontWeight = 600,
			LineHeight = 1,
			Outline = new TextRendering.Outline() { Color = Color.Black, Enabled = true, Size = 3 }
		};

		var offset = Vector2.Up * 24;
		var brush = _parent.BrushSettings;

		if ( Gizmo.IsShiftPressed )
		{
			textScope.Text = $"Size: {brush.Size}";
			Gizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset );

			textScope.Text = $"Opacity: {brush.Opacity:0.##}";
			Gizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset * 2 );

			if ( Mode == SubTerrainSculptMode.RaiseLower )
			{
				textScope.Text = $"Speed: {brush.Speed}";
				Gizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset * 3 );
			}
		}
		else if ( Gizmo.IsCtrlPressed && !brush.RandomRotation )
		{
			textScope.Text = $"Rotation: {brush.Rotation:0.#}";
			Gizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset );
		}
	}

	protected virtual void DrawToolOverlay( Terrain terrain, Vector3 worldCenter )
	{
	}

	protected virtual void OnStrokeStart( Terrain terrain, Vector3 hitPosition )
	{
	}

	/// <summary>
	/// World Size is brush radius; dispatch covers the diameter (Size * 2 in world units).
	/// </summary>
	protected int BrushTexelSize( Terrain terrain, float worldSize )
	{
		int size = (int)Math.Floor( worldSize * 2.0f / terrain.Storage.TerrainSize * terrain.Storage.Resolution );
		return Math.Max( size, 1 );
	}

	protected void GrowDirtyRegion( Terrain terrain, Vector2 hitUV, int size )
	{
		var x = (int)Math.Floor( terrain.Storage.Resolution * hitUV.x ) - size / 2;
		var y = (int)Math.Floor( terrain.Storage.Resolution * hitUV.y ) - size / 2;
		_dirtyRegion.Add( new RectInt( x, y, size + 1, size + 1 ) );
	}

	protected virtual void OnPaint( Terrain terrain, SubTerrainPaintParameters paint )
	{
		var brushTexture = _parent.SelectedBrushTexture;
		if ( brushTexture is null )
			return;

		int size = BrushTexelSize( terrain, paint.BrushSettings.Size );
		var amount = paint.BrushSettings.Opacity;
		if ( Mode == SubTerrainSculptMode.RaiseLower )
		{
			// Speed 50 = historical 1x height per stamp; 100 ≈ 4x. Hold rate is separate.
			amount *= Math.Clamp( paint.BrushSettings.Speed / 50f, 0.02f, 4f );
		}

		var strength = amount * (AllowBrushInvert && Gizmo.IsCtrlPressed ? -1.0f : 1.0f);

		DispatchSculpt( terrain, paint, brushTexture, size, strength );
	}

	protected void DispatchSculpt( Terrain terrain, SubTerrainPaintParameters paint, Texture brushTexture, int size, float strength )
	{
		if ( (int)Mode <= (int)SubTerrainSculptMode.Noise )
		{
			DispatchEngineSculpt( terrain, paint, brushTexture, size, strength );
			return;
		}

		var cs = new ComputeShader( "terrain/cs_subterrain_sculpt" );

		cs.Attributes.SetComboEnum( "D_SCULPT_MODE", Mode );

		cs.Attributes.Set( "Heightmap", terrain.HeightMap );
		cs.Attributes.Set( "ControlMap", terrain.ControlMap );

		cs.Attributes.Set( "HeightUV", paint.HitUV );
		cs.Attributes.Set( "FlattenHeight", paint.FlattenHeight );
		cs.Attributes.Set( "SlopeStartUV", SlopeStartUV );
		cs.Attributes.Set( "SlopeEndUV", paint.HitUV );
		cs.Attributes.Set( "SlopeStartHeight", SlopeStartHeight01 );
		cs.Attributes.Set( "SlopeEndHeight", paint.FlattenHeight );
		cs.Attributes.Set( "BrushStrength", strength );
		cs.Attributes.Set( "BrushSize", size );
		cs.Attributes.Set( "Brush", brushTexture );

		ApplyExtraAttributes( cs, paint );

		cs.Dispatch( size, size, 1 );

		GrowDirtyRegion( terrain, paint.HitUV, size );
	}

	/// <summary>
	/// Modes 0-4 match the engine sculpt shader, which already supports brush rotation.
	/// Avoids recompiling cs_subterrain_sculpt (that compile has been crashing the editor).
	/// </summary>
	void DispatchEngineSculpt( Terrain terrain, SubTerrainPaintParameters paint, Texture brushTexture, int size, float strength )
	{
		var cs = new ComputeShader( "terrain/cs_terrain_sculpt" );

		cs.Attributes.SetComboEnum( "D_SCULPT_MODE", (SculptMode)(int)Mode );

		cs.Attributes.Set( "Heightmap", terrain.HeightMap );
		cs.Attributes.Set( "ControlMap", terrain.ControlMap );

		_engineBrushBuffer ??= new GpuBuffer<BrushData>( 1 );
		_engineBrushBuffer.SetData( new[]
		{
			new BrushData
			{
				UV = paint.HitUV,
				Strength = strength,
				Size = size,
				Rotation = paint.BrushSettings.Rotation * MathF.PI / 180f,
				FlattenHeight = paint.FlattenHeight,
			}
		} );
		cs.Attributes.Set( "BrushSettings", _engineBrushBuffer );
		cs.Attributes.Set( "Brush", brushTexture );

		cs.Dispatch( size, size, 1 );

		GrowDirtyRegion( terrain, paint.HitUV, size );
	}

	/// <summary>
	/// Set any attributes a sub-tool needs on top of the shared sculpt ones.
	/// Runs after the shared attributes, so it can override them.
	/// </summary>
	protected virtual void ApplyExtraAttributes( ComputeShader cs, SubTerrainPaintParameters paint )
	{
	}

	private static T[] CopyRegion<T>( T[] data, int stride, RectInt rect ) where T : unmanaged
	{
		T[] region = new T[rect.Width * rect.Height];

		for ( int y = 0; y < rect.Height; y++ )
		{
			for ( int x = 0; x < rect.Width; x++ )
			{
				region[x + y * rect.Width] = data[rect.Left + x + (rect.Top + y) * stride];
			}
		}

		return region;
	}

	private static Action CreateUndoAction<T>( Terrain terrain, T[] dest, T[] region, RectInt dirtyRegion, Terrain.SyncFlags flags ) => () =>
	{
		if ( !terrain.IsValid() )
			return;

		for ( int y = 0; y < dirtyRegion.Height; y++ )
		{
			for ( int x = 0; x < dirtyRegion.Width; x++ )
			{
				dest[dirtyRegion.Left + x + (dirtyRegion.Top + y) * terrain.Storage.Resolution] = region[x + y * dirtyRegion.Width];
			}
		}

		terrain.SyncGPUTexture();
		terrain.UpdateCollision( flags, dirtyRegion );
	};

	protected virtual void OnPaintEnded( Terrain terrain )
	{
		_dirtyRegion.Left = Math.Clamp( _dirtyRegion.Left, 0, terrain.Storage.Resolution - 1 );
		_dirtyRegion.Right = Math.Clamp( _dirtyRegion.Right, 0, terrain.Storage.Resolution - 1 );
		_dirtyRegion.Top = Math.Clamp( _dirtyRegion.Top, 0, terrain.Storage.Resolution - 1 );
		_dirtyRegion.Bottom = Math.Clamp( _dirtyRegion.Bottom, 0, terrain.Storage.Resolution - 1 );

		var name = $"Terrain {DisplayInfo.For( this ).Name}";

		if ( Mode != SubTerrainSculptMode.Hole )
		{
			var regionBefore = CopyRegion( terrain.Storage.HeightMap, terrain.Storage.Resolution, _dirtyRegion );
			terrain.SyncCPUTexture( Terrain.SyncFlags.Height, _dirtyRegion );
			var regionAfter = CopyRegion( terrain.Storage.HeightMap, terrain.Storage.Resolution, _dirtyRegion );

			SceneEditorSession.Active.UndoSystem.Insert( name,
				CreateUndoAction( terrain, terrain.Storage.HeightMap, regionBefore, _dirtyRegion, Terrain.SyncFlags.Height ),
				CreateUndoAction( terrain, terrain.Storage.HeightMap, regionAfter, _dirtyRegion, Terrain.SyncFlags.Height ) );
		}
		else
		{
			var regionBefore = CopyRegion( terrain.Storage.ControlMap, terrain.Storage.Resolution, _dirtyRegion );
			terrain.SyncCPUTexture( Terrain.SyncFlags.Control, _dirtyRegion );
			var regionAfter = CopyRegion( terrain.Storage.ControlMap, terrain.Storage.Resolution, _dirtyRegion );

			SceneEditorSession.Active.UndoSystem.Insert( name,
				CreateUndoAction( terrain, terrain.Storage.ControlMap, regionBefore, _dirtyRegion, Terrain.SyncFlags.Control ),
				CreateUndoAction( terrain, terrain.Storage.ControlMap, regionAfter, _dirtyRegion, Terrain.SyncFlags.Control ) );
		}
	}
}
subzerostudios.subterrain-tool / TimedDestroy.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;

namespace SubZero.SubTerrain;

/// <summary>
/// Destroys this GameObject after a delay. Used by debris prefabs.
/// </summary>
[Title( "Timed Destroy" )]
[Category( "SubTerrain" )]
[Icon( "timer" )]
public sealed class TimedDestroy : Component
{
	[Property] public float Seconds { get; set; } = 12f;

	TimeSince _alive;

	protected override void OnStart()
	{
		_alive = 0;
	}

	protected override void OnUpdate()
	{
		if ( _alive >= Seconds )
			GameObject.Destroy();
	}
}
subzerostudios.subterrain-tool / Code/TimedDestroy.cs
Game library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;

namespace SubZero.SubTerrain;

/// <summary>
/// Destroys this GameObject after a delay. Used by debris prefabs.
/// </summary>
[Title( "Timed Destroy" )]
[Category( "SubTerrain" )]
[Icon( "timer" )]
public sealed class TimedDestroy : Component
{
	[Property] public float Seconds { get; set; } = 12f;

	TimeSince _alive;

	protected override void OnStart()
	{
		_alive = 0;
	}

	protected override void OnUpdate()
	{
		if ( _alive >= Seconds )
			GameObject.Destroy();
	}
}
subzerostudios.subterrain-tool / Editor/Tools/PathTool.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using Sandbox;

namespace Editor.SubTerrain;

/// <summary>
/// Keeps the click height along the stroke. Paint Texture Path is the control-map path.
/// </summary>
[Title( "Path" )]
[Icon( "route" )]
[Alias( "subterrain_path" )]
[Group( "1" )]
[Order( 6 )]
public class SubPathTool : SubTerrainBaseBrushTool
{
	Vector3 _startLocal;

	public SubPathTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Path;
	}

	protected override void OnStrokeStart( Terrain terrain, Vector3 hitPosition )
	{
		_startLocal = hitPosition;
		SlopeStartUV = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize;
		SlopeStartHeight01 = hitPosition.z / terrain.Storage.TerrainHeight;
	}

	protected override void DrawToolOverlay( Terrain terrain, Vector3 worldCenter )
	{
		if ( !_dragging )
			return;

		var startWorld = terrain.WorldTransform.PointToWorld( _startLocal );
		Gizmo.Draw.Color = Color.FromBytes( 120, 200, 250 );
		Gizmo.Draw.Line( startWorld, worldCenter );
	}
}
subzerostudios.subterrain-tool / Editor/SubTerrainBrushSize.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;

namespace Editor.SubTerrain;

/// <summary>
/// Brush Size limits and piecewise track mapping.
/// Fine band stays precise; larger sizes take less track and bigger nudges.
/// </summary>
public static class SubTerrainBrushSize
{
	public const int Min = 8;
	public const int FineMax = 3500;
	public const int MidMax = 18000;
	public const int Max = 35000;

	public const int FineStep = 50;
	public const int MidStep = 500;
	public const int CoarseStep = 1000;

	/// <summary>Fraction of the slider track for the fine band (8..FineMax).</summary>
	public const float FineTrack = 0.70f;

	/// <summary>End of mid band on the track (FineMax..MidMax). Coarse is the rest to 1.</summary>
	public const float MidTrack = 0.90f;

	public static int Clamp( int size ) => Math.Clamp( size, Min, Max );

	public static int StepFor( int size )
	{
		if ( size < FineMax )
			return FineStep;
		if ( size < MidMax )
			return MidStep;
		return CoarseStep;
	}

	public static int Nudge( int size, int direction )
	{
		if ( direction == 0 )
			return Clamp( size );

		size = Clamp( size );
		int step = StepFor( size );
		return Clamp( size + direction * step );
	}

	public static int SizeFromTrack( float t01 )
	{
		t01 = Math.Clamp( t01, 0f, 1f );

		int raw;
		if ( t01 <= FineTrack )
		{
			float u = FineTrack <= 0f ? 0f : t01 / FineTrack;
			raw = (int)MathF.Round( Lerp( Min, FineMax, u ) );
		}
		else if ( t01 <= MidTrack )
		{
			float u = (t01 - FineTrack) / (MidTrack - FineTrack);
			raw = (int)MathF.Round( Lerp( FineMax, MidMax, u ) );
		}
		else
		{
			float u = (t01 - MidTrack) / (1f - MidTrack);
			raw = (int)MathF.Round( Lerp( MidMax, Max, u ) );
		}

		return Snap( raw );
	}

	public static float TrackFromSize( int size )
	{
		size = Clamp( size );

		if ( size <= FineMax )
		{
			float u = (size - Min) / (float)(FineMax - Min);
			return u * FineTrack;
		}

		if ( size <= MidMax )
		{
			float u = (size - FineMax) / (float)(MidMax - FineMax);
			return FineTrack + u * (MidTrack - FineTrack);
		}

		{
			float u = (size - MidMax) / (float)(Max - MidMax);
			return MidTrack + u * (1f - MidTrack);
		}
	}

	public static int Snap( int size )
	{
		size = Clamp( size );
		int step = StepFor( size );
		int snapped = (int)MathF.Round( size / (float)step ) * step;
		return Clamp( snapped );
	}

	static float Lerp( float a, float b, float t ) => a + (b - a) * Math.Clamp( t, 0f, 1f );
}
subzerostudios.subterrain-tool / Editor/SubTerrainTool.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Editor.TerrainEditor;

namespace Editor.SubTerrain;

/// <summary>
/// SubTerrain editor tool. Extends the built-in terrain workflow with larger brushes,
/// slope/path/set-height sculpt modes, and paint texture support.
/// </summary>
[EditorTool( "tools.subterrain" )]
[Title( "SubTerrain" )]
[Icon( "terrain" )]
[Alias( "subterrain" )]
[Group( "Scene" )]
public class SubTerrainTool : EditorTool
{
	public static SubTerrainTool ActiveInstance { get; private set; }

	public SubTerrainBrushSettings BrushSettings { get; private set; } = SubTerrainBrushStore.ForMode( nameof( SubRaiseLowerTool ) );
	public SubTerrainPaintSettings PaintSettings { get; private set; } = new();
	public bool PaintArmed { get; set; }
	string _brushMode = nameof( SubRaiseLowerTool );

	BrushPreviewSceneObject _previewObject;
	SubTerrainStatusWidget _statusWidget;
	Widget _opacityWidget;
	Widget _speedWidget;
	Widget _rotationWidget;
	Widget _brushSheetHost;
	SubTerrainBrushSizeWidget _sizeWidget;
	Widget _sidebar;
	Widget _textureSection;
	Widget _statusSection;
	SubTerrainBlendWidget _blendWidget;
	Label _intentLabel;

	public Terrain PeekTerrain()
	{
		if ( StatusTerrain.IsValid() )
			return StatusTerrain;

		return GetSelectedComponent<Terrain>();
	}

	public void SetBrushMode( string key )
	{
		if ( string.IsNullOrEmpty( key ) )
			key = nameof( SubRaiseLowerTool );

		var next = SubTerrainBrushStore.ForMode( key );
		bool same = _brushMode == key && BrushSettings == next;
		_brushMode = key;
		BrushSettings = next;

		if ( !same )
		{
			RebuildBrushSheet();
			UpdateIntent();
		}

		UpdateOpacityState();
	}

	public void RefreshPaintChrome()
	{
		if ( _textureSection is not null )
			_textureSection.Visible = PaintArmed;

		if ( _blendWidget is not null )
			_blendWidget.Visible = PaintArmed;

		if ( _statusSection is not null )
			_statusSection.Visible = !PaintArmed;

		UpdateOpacityState();
		UpdateIntent();
	}

	public void UpdateIntent()
	{
		if ( _intentLabel is null )
			return;

		if ( PaintArmed )
		{
			_intentLabel.Text = "Paint Texture.";
			_intentLabel.ToolTip = "Alt sample | Ctrl fade | Shift smooth | ring yellow/green = Alt/Shift. Modes and size keys are in Shortcuts below.";
			return;
		}

		_intentLabel.Text = IntentForSculpt();
		_intentLabel.ToolTip = IntentTooltipForSculpt();
	}

	string IntentForSculpt()
	{
		return _brushMode switch
		{
			nameof( SubSmoothTool ) => "Smooth height.",
			nameof( SubPlainerTool ) => "Plainer: broad weighted leveling.",
			nameof( SubFlattenTool ) => "Flatten to click Z.",
			nameof( SubSlopeTool ) => "Slope: click start, drag to end.",
			nameof( SubPathTool ) => "Path: constant elevation strip.",
			nameof( SubSetHeightTool ) => "Flatten to Target Height.",
			nameof( SubNoiseTool ) => "Noise bump. Ctrl inverts.",
			nameof( SubHoleTool ) => "Punch hole. Ctrl fills.",
			nameof( SubBridgeTool ) => "Bridge: press start, release on end.",
			_ => "Raise height. Ctrl lowers."
		};
	}

	string IntentTooltipForSculpt()
	{
		var tip = _brushMode switch
		{
			nameof( SubPathTool ) => "Height-only strip. Control-map Path is Paint Texture (Ctrl+3).",
			nameof( SubSetHeightTool ) => "Alt-click loads Z into Target Height.",
			nameof( SubPlainerTool ) => "Like Hammer displacement smoothing across a wide brush.",
			nameof( SubBridgeTool ) => "Deck sits on the line between the two picked heights. Shortcut B.",
			_ => null
		};

		var keys = "Left / Right or Ctrl+[ / ] brush size. N activates SubTerrain. Keys 1-9, 0 Plainer, B Bridge.";
		return tip is null ? keys : $"{tip} {keys}";
	}

	/// <summary>
	/// Scene view destroys sidebar children on every subtool switch. Unparent first
	/// from subtool OnDisabled so we can reuse one panel and avoid width/content flash.
	/// </summary>
	public void DetachSidebarForReuse()
	{
		if ( _sidebar is not null && _sidebar.IsValid() )
			_sidebar.Parent = null;
	}

	public void NudgeBrushSize( int direction )
	{
		BrushSettings.Size = SubTerrainBrushSize.Nudge( BrushSettings.Size, direction );
		SyncBrushSizeWidget();
	}

	/// <summary>
	/// Sculpt modes only. Paint Texture keeps Up / Down for tmat selection.
	/// </summary>
	public bool NudgeBrushOpacity( int direction )
	{
		if ( PaintArmed || direction == 0 )
			return false;

		const float step = 0.05f;
		BrushSettings.Opacity = Math.Clamp( BrushSettings.Opacity + direction * step, 0f, 1f );
		RebuildBrushSheet();
		return true;
	}

	public void SyncBrushSizeWidget()
	{
		_sizeWidget?.SyncFromSettings();
	}

	public void SyncBrushRotationRows()
	{
		if ( _rotationWidget is not null )
			_rotationWidget.Enabled = !BrushSettings.RandomRotation;
	}

	public bool TrySetPaintLayer( SubTerrainPaintLayer layer )
	{
		if ( !PaintArmed || _blendWidget is null )
			return false;

		if ( layer == SubTerrainPaintLayer.Base )
			_blendWidget.SelectGround();
		else if ( layer == SubTerrainPaintLayer.Overlay )
			_blendWidget.SelectWear();
		else
			_blendWidget.SelectPath();

		return true;
	}

	/// <summary>
	/// Cycle selected paint tmat. Path skips the locked ground row (that role is Alt-click).
	/// </summary>
	public bool NudgePaintMaterial( int delta )
	{
		if ( !PaintArmed || delta == 0 )
			return false;

		var terrain = PeekTerrain();
		var materials = terrain?.Storage?.Materials;
		int count = materials?.Count ?? 0;
		if ( count <= 0 )
			return false;

		int skip = -1;
		if ( PaintSettings.Layer == SubTerrainPaintLayer.Path )
			skip = SubTerrainSplatUtil.FindPathGround( terrain, PaintSettings );

		int current = PaintSettings.MaterialIndex;
		if ( current < 0 || current >= count )
			current = 0;

		for ( int step = 0; step < count; step++ )
		{
			current = (current + delta) % count;
			if ( current < 0 )
				current += count;

			if ( current == skip )
				continue;

			PaintSettings.MaterialIndex = current;
			SubTerrainPaintTool.TrySetSplatChannel( current );
			_blendWidget?.OnMaterialSelectionChanged();
			return true;
		}

		return false;
	}

	public bool StatusValid { get; private set; }
	public Terrain StatusTerrain { get; private set; }
	public Vector3 StatusHitLocal { get; private set; }

	public static BrushList BrushList => TerrainEditorTool.BrushList;

	public Texture SelectedBrushTexture => BrushList?.Selected?.Texture;

	public void UpdateStatus( Terrain terrain, Vector3 hitLocal )
	{
		StatusValid = true;
		StatusTerrain = terrain;
		StatusHitLocal = hitLocal;
	}

	public void ClearStatus()
	{
		StatusValid = false;
		StatusTerrain = default;
	}

	public override IEnumerable<EditorTool> GetSubtools()
	{
		yield return new SubRaiseLowerTool( this );
		yield return new SubSmoothTool( this );
		yield return new SubPlainerTool( this );
		yield return new SubFlattenTool( this );
		yield return new SubTerrainPaintTool( this );
		yield return new SubSlopeTool( this );
		yield return new SubPathTool( this );
		yield return new SubSetHeightTool( this );
		yield return new SubNoiseTool( this );
		yield return new SubHoleTool( this );
		yield return new SubBridgeTool( this );
	}

	public override void OnEnabled()
	{
		ActiveInstance = this;
		AllowGameObjectSelection = false;
		RebuildSidebarOnSelectionChange = false;

		var selectedTerrain = GetSelectedComponent<Terrain>();
		if ( !selectedTerrain.IsValid() )
		{
			Selection.Clear();
			var first = Scene.GetAllComponents<Terrain>().FirstOrDefault();
			if ( first.IsValid() ) Selection.Add( first.GameObject );
		}
	}

	public override void OnDisabled()
	{
		if ( ActiveInstance == this )
			ActiveInstance = null;

		if ( _sidebar is not null && _sidebar.IsValid() )
		{
			_sidebar.Parent = null;
			_sidebar.Destroy();
		}

		_sidebar = null;
		_blendWidget = null;
		_intentLabel = null;
		_brushSheetHost = null;
		_sizeWidget = null;
		_opacityWidget = null;
		_rotationWidget = null;
		_textureSection = null;
		_statusSection = null;
		_statusWidget = null;

		_previewObject?.Delete();
		_previewObject = null;
		ClearStatus();
	}

	public override Widget CreateShortcutsWidget() => new SubTerrainToolShortcutsWidget();

	[Shortcut( "tools.subterrain", "n", typeof( SceneViewWidget ) )]
	public static void ActivateTool()
	{
		EditorToolManager.SetTool( nameof( SubTerrainTool ) );
	}

	public void DrawBrushPreview( Transform transform, Terrain terrain = null )
	{
		var brush = BrushList?.Selected?.Texture;
		if ( brush is null )
			return;

		_previewObject ??= new BrushPreviewSceneObject( Gizmo.World );

		var color = Color.FromBytes( 150, 150, 250 );
		var mods = Application.KeyboardModifiers;
		if ( mods.HasFlag( KeyboardModifiers.Alt ) )
			color = Color.FromBytes( 220, 200, 80 );
		else if ( mods.HasFlag( KeyboardModifiers.Shift ) )
			color = Color.FromBytes( 120, 200, 140 );
		else if ( mods.HasFlag( KeyboardModifiers.Ctrl ) )
			color = color.AdjustHue( 90 );

		color.a = BrushSettings.Opacity;

		_previewObject.RenderLayer = SceneRenderLayer.OverlayWithDepth;
		_previewObject.Bounds = BBox.FromPositionAndSize( 0, float.MaxValue );
		_previewObject.Transform = transform;
		_previewObject.Radius = BrushSettings.Size;
		_previewObject.Texture = brush;
		_previewObject.Color = color;
		_previewObject.BrushRotation = BrushSettings.Rotation;

		terrain ??= PeekTerrain();
		if ( BrushSettings.ShowGridPreview && terrain?.Storage is not null )
		{
			var tx = terrain.WorldTransform;
			_previewObject.CellSize = terrain.Storage.TerrainSize / terrain.Storage.Resolution;
			_previewObject.TerrainOrigin = tx.Position;
			_previewObject.TerrainRight = tx.Rotation.Right;
			_previewObject.TerrainForward = tx.Rotation.Forward;
		}
		else
		{
			_previewObject.CellSize = 0f;
		}
	}

	public override Widget CreateToolSidebar()
	{
		// Sidebar reuse: detach in OnDisabled before the scene view Clears the host.
		if ( _sidebar is ToolSidebarWidget existing && existing.IsValid() )
		{
			existing.Parent = null;
			RebuildBrushSheet();
			RefreshPaintChrome();
			return existing;
		}

		var sidebar = new ToolSidebarWidget();
		_sidebar = sidebar;
		sidebar.AddTitle( "SubTerrain", "terrain" );
		sidebar.ToolTip = $"SubTerrain {SubTerrainPaintSettings.LibraryVersion}";

		_intentLabel = sidebar.Layout.Add( new Label( "" ) );
		_intentLabel.Color = Theme.TextControl;
		sidebar.Layout.AddSpacingCell( 6 );

		_textureSection = new Widget( sidebar );
		_textureSection.Layout = Layout.Column();
		_blendWidget = new SubTerrainBlendWidget( this, _textureSection );
		_textureSection.Layout.Add( _blendWidget );
		sidebar.Layout.Add( _textureSection );
		_textureSection.Visible = false;

		{
			var group = sidebar.AddGroup( "Brush Type" );
			group.Add( new SubTerrainBrushPreviewWidget( sidebar ) );
		}

		{
			var group = sidebar.AddGroup( "Brush Properties" );
			_brushSheetHost = new Widget( sidebar );
			_brushSheetHost.Layout = Layout.Column();
			group.Add( _brushSheetHost );
			RebuildBrushSheet();
		}

		UpdateOpacityState();

		if ( !GetSelectedComponent<Terrain>().IsValid() )
		{
			var group = sidebar.AddGroup( "No Terrain" );
			group.Add( new Label( "Select a GameObject with a Terrain component." ) );
		}

		{
			var cursorLayout = sidebar.AddGroup( "Cursor" );
			_statusWidget = new SubTerrainStatusWidget( this, sidebar );
			cursorLayout.Add( _statusWidget );
			_statusSection = FindSidebarGroupHost( _statusWidget );
		}

		sidebar.AddShortcuts(
			("Activate SubTerrain", "N"),
			("Sculpt / paint tools", "1 - 9"),
			("Plainer / Bridge", "0 / B"),
			("Paint: Ground / Wear / Path", "Ctrl+1 / 2 / 3"),
			("Paint tmat prev / next", "Up / Down"),
			("Opacity (sculpt)", "Up / Down"),
			("Brush size", "Left / Right | Ctrl+[ / ]"),
			("Size / opacity", "MMB+Shift drag"),
			("Brush rotation", "MMB+Ctrl drag"),
			("Sample / fade / smooth", "Alt / Ctrl / Shift")
		);

		sidebar.Layout.AddStretchCell();
		UpdateIntent();
		return sidebar;
	}

	static Widget FindSidebarGroupHost( Widget child )
	{
		for ( var p = child?.Parent; p is not null; p = p.Parent )
		{
			if ( p.GetType().Name == "SidebarGroupWidget" )
				return p;
		}

		return child?.Parent;
	}

	[EditorEvent.Frame]
	public void TickSidebarChrome()
	{
		if ( ActiveInstance != this )
			return;

		if ( !PaintArmed )
			UpdateIntent();
	}

	void RebuildBrushSheet()
	{
		if ( _brushSheetHost is null )
			return;

		_brushSheetHost.Layout.Clear( true );
		_sizeWidget = null;
		_opacityWidget = null;
		_speedWidget = null;
		_rotationWidget = null;

		_sizeWidget = new SubTerrainBrushSizeWidget( this, _brushSheetHost );
		_brushSheetHost.Layout.Add( _sizeWidget );

		var so = BrushSettings.GetSerialized();
		var sheet = new ControlSheet();
		_opacityWidget = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.Opacity ) ) );
		if ( _brushMode == nameof( SubRaiseLowerTool ) )
			_speedWidget = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.Speed ) ) );
		_rotationWidget = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.Rotation ) ) );
		var rndRot = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.RandomRotation ) ) );
		sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.ShowGridPreview ) ) );
		if ( _brushMode == nameof( SubSetHeightTool ) )
			sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.TargetHeight ) ) );
		if ( _brushMode == nameof( SubBridgeTool ) )
		{
			sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.BridgeWidth ) ) );
			sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.BridgeEdgeFalloff ) ) );
			sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.BridgeRaiseOnly ) ) );
		}

		so.OnPropertyChanged += OnBrushSheetPropertyChanged;

		_brushSheetHost.Layout.Add( sheet );
		SyncBrushRotationRows();
		UpdateOpacityState();
	}

	void OnBrushSheetPropertyChanged( SerializedProperty prop )
	{
		if ( prop?.Name == nameof( SubTerrainBrushSettings.RandomRotation ) )
			SyncBrushRotationRows();
	}

	void UpdateOpacityState()
	{
		if ( _speedWidget is not null )
			_speedWidget.ToolTip = "How fast height accumulates. Hold still to keep raising. Higher = more stamps and more height per stamp.";

		if ( _opacityWidget is null )
			return;

		if ( !PaintArmed )
		{
			bool bridge = _brushMode == nameof( SubBridgeTool );
			_opacityWidget.Enabled = !bridge;
			_opacityWidget.ToolTip = bridge
				? "Bridge always builds at the exact start and end heights."
				: OpacityTipForSculpt();
			return;
		}

		var layer = PaintSettings.Layer;
		if ( layer == SubTerrainPaintLayer.Overlay )
		{
			_opacityWidget.Enabled = true;
			_opacityWidget.ToolTip = "How fast wear builds on this stroke. Ctrl reduces.";
			return;
		}

		_opacityWidget.Enabled = false;
		_opacityWidget.ToolTip = layer == SubTerrainPaintLayer.Path
			? "Path uses Center mix and Core for the apron / solid ribbon. Pack center optional. Soft circle or stroke."
			: "Ground paint lays a solid tmat. Mix is cleared.";
	}

	string OpacityTipForSculpt()
	{
		return _brushMode switch
		{
			nameof( SubRaiseLowerTool ) => "Brush soft strength. Ctrl lowers. Use Speed for how fast height builds.",
			nameof( SubHoleTool ) => "How fast the punch cuts. Ctrl fills the hole back.",
			nameof( SubNoiseTool ) => "How strong the bump is. Ctrl inverts.",
			nameof( SubFlattenTool ) => "How fast it levels to the click height.",
			nameof( SubSetHeightTool ) => "How fast it levels to Target Height.",
			nameof( SubSlopeTool ) => "How fast it grades to the ramp.",
			nameof( SubPathTool ) => "How fast it levels to the click height.",
			nameof( SubSmoothTool ) => "How strong the smooth is.",
			nameof( SubPlainerTool ) => "How strong the weighted leveling is.",
			_ => "Stroke strength."
		};
	}
}

[Title( "Raise / Lower" )]
[Description( "Paint height up. Hold Ctrl to lower." )]
[Icon( "height" )]
[Alias( "subterrain_raise_lower" )]
[Group( "1" )]
[Order( 0 )]
public class SubRaiseLowerTool : SubTerrainBaseBrushTool
{
	public SubRaiseLowerTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.RaiseLower;
		AllowBrushInvert = true;
	}
}

[Title( "Flatten" )]
[Description( "Level to the height under the click. Stays on that plane while you drag." )]
[Icon( "trending_flat" )]
[Alias( "subterrain_flatten" )]
[Group( "1" )]
[Order( 3 )]
public class SubFlattenTool : SubTerrainBaseBrushTool
{
	public SubFlattenTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Flatten;
	}

	public override bool GetHitPosition( Terrain terrain, out Vector3 position )
	{
		if ( _dragging )
		{
			var tx = terrain.WorldTransform;
			var hit = StrokePlane.Trace( Gizmo.CurrentRay, true );
			position = tx.PointToLocal( hit.Value );
			return hit.HasValue;
		}

		return base.GetHitPosition( terrain, out position );
	}
}

[Title( "Smooth" )]
[Description( "Blend neighbouring heights." )]
[Icon( "rounded_corner" )]
[Alias( "subterrain_smooth" )]
[Group( "1" )]
[Order( 1 )]
public class SubSmoothTool : SubTerrainBaseBrushTool
{
	public SubSmoothTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Smooth;
	}

	public override bool GetHitPosition( Terrain terrain, out Vector3 position )
	{
		if ( _dragging )
		{
			var tx = terrain.WorldTransform;
			var hit = StrokePlane.Trace( Gizmo.CurrentRay, true );
			position = tx.PointToLocal( hit.Value );
			return hit.HasValue;
		}

		return base.GetHitPosition( terrain, out position );
	}
}

[Title( "Plainer" )]
[Description( "Gradually level a broad weighted area, like Hammer displacement smoothing." )]
[Icon( "blur_on" )]
[Alias( "subterrain_plainer" )]
[Group( "1" )]
[Order( 2 )]
public class SubPlainerTool : SubTerrainBaseBrushTool
{
	public SubPlainerTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Plainer;
	}

	public override bool GetHitPosition( Terrain terrain, out Vector3 position )
	{
		if ( _dragging )
		{
			var tx = terrain.WorldTransform;
			var hit = StrokePlane.Trace( Gizmo.CurrentRay, true );
			position = tx.PointToLocal( hit.Value );
			return hit.HasValue;
		}

		return base.GetHitPosition( terrain, out position );
	}
}

[Title( "Noise" )]
[Description( "Add bump. Hold Ctrl to invert." )]
[Icon( "grain" )]
[Alias( "subterrain_noise" )]
[Group( "1" )]
[Order( 8 )]
public class SubNoiseTool : SubTerrainBaseBrushTool
{
	public SubNoiseTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Noise;
		AllowBrushInvert = true;
	}
}

[Title( "Hole" )]
[Description( "Punch through the surface. Hold Ctrl to fill the hole back." )]
[Icon( "trip_origin" )]
[Alias( "subterrain_hole" )]
[Group( "1" )]
[Order( 9 )]
public class SubHoleTool : SubTerrainBaseBrushTool
{
	public SubHoleTool( SubTerrainTool parent ) : base( parent )
	{
		Mode = SubTerrainSculptMode.Hole;
		AllowBrushInvert = true;
	}
}

internal class SubTerrainBrushPreviewWidget : Widget
{
	public SubTerrainBrushPreviewWidget( Widget parent ) : base( parent )
	{
		MinimumSize = new( 32, 32 );
		MaximumSize = new( 48, 48 );
		Cursor = CursorShape.Finger;
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		Paint.Antialiasing = true;
		Paint.ClearPen();
		Paint.DrawRect( LocalRect );

		var pixmap = SubTerrainTool.BrushList?.Selected?.Pixmap;
		if ( pixmap != null )
		{
			Paint.Draw( LocalRect.Contain( pixmap.Size ), pixmap );
		}
	}

	protected override void OnMouseClick( MouseEvent e )
	{
		var popup = new PopupWidget( null );
		popup.Position = Application.CursorPosition;
		popup.Visible = true;
		popup.Layout = Layout.Column();
		popup.Layout.Margin = 10;
		popup.MaximumSize = new Vector2( 300, 200 );

		var list = new SubTerrainBrushListWidget();
		list.BrushSelected += () =>
		{
			popup.Close();
			Update();
		};
		popup.Layout.Add( list );
	}

	[EditorEvent.Frame]
	public void UpdatePreview()
	{
		Update();
	}
}

internal class SubTerrainBrushListWidget : Widget
{
	public Action BrushSelected { get; set; }

	public SubTerrainBrushListWidget() : base( null )
	{
		ListView list = new();
		list.SetItems( SubTerrainTool.BrushList.Brushes.Cast<object>() );
		list.ItemSize = new Vector2( 40, 40 );
		list.ItemAlign = Sandbox.UI.Align.SpaceBetween;
		list.OnPaintOverride += () => PaintListBackground( list );
		list.ItemPaint = PaintBrushItem;
		list.ItemSelected = item =>
		{
			if ( item is Brush brush )
			{
				SubTerrainTool.BrushList.Selected = brush;
				BrushSelected?.Invoke();
			}
		};
		list.SelectItem( SubTerrainTool.BrushList.Selected );

		Layout = Layout.Column();

		var label = new Label( "Brushes" );
		label.SetStyles( "font-weight: bold" );
		Layout.Add( label );
		Layout.AddSpacingCell( 8 );
		Layout.Add( list );
	}

	private void PaintBrushItem( VirtualWidget widget )
	{
		var brush = (Brush)widget.Object;

		Paint.Antialiasing = true;
		Paint.TextAntialiasing = true;

		if ( widget.Hovered || widget.Selected )
		{
			Paint.ClearPen();
			Paint.SetBrush( widget.Selected ? Theme.Primary : Color.White.WithAlpha( 0.1f ) );
			Paint.DrawRect( widget.Rect.Grow( 2 ), 3 );
		}

		Paint.Draw( widget.Rect, brush.Pixmap );
	}

	private bool PaintListBackground( Widget widget )
	{
		Paint.ClearPen();
		Paint.SetBrush( Theme.ControlBackground );
		Paint.DrawRect( widget.LocalRect );
		return false;
	}
}

internal class SubTerrainToolShortcutsWidget : Widget
{
	[Shortcut( "tools.subterrain.raise-lower", "1", typeof( SceneViewWidget ) )]
	public void ActivateRaiseLower() => EditorToolManager.SetSubTool( nameof( SubRaiseLowerTool ) );

	[Shortcut( "tools.subterrain.smooth", "2", typeof( SceneViewWidget ) )]
	public void ActivateSmooth() => EditorToolManager.SetSubTool( nameof( SubSmoothTool ) );

	[Shortcut( "tools.subterrain.flatten", "3", typeof( SceneViewWidget ) )]
	public void ActivateFlatten() => EditorToolManager.SetSubTool( nameof( SubFlattenTool ) );

	[Shortcut( "tools.subterrain.paint", "4", typeof( SceneViewWidget ) )]
	public void ActivatePaint() => EditorToolManager.SetSubTool( nameof( SubTerrainPaintTool ) );

	[Shortcut( "tools.subterrain.slope", "5", typeof( SceneViewWidget ) )]
	public void ActivateSlope() => EditorToolManager.SetSubTool( nameof( SubSlopeTool ) );

	[Shortcut( "tools.subterrain.path", "6", typeof( SceneViewWidget ) )]
	public void ActivatePath() => EditorToolManager.SetSubTool( nameof( SubPathTool ) );

	[Shortcut( "tools.subterrain.set-height", "7", typeof( SceneViewWidget ) )]
	public void ActivateSetHeight() => EditorToolManager.SetSubTool( nameof( SubSetHeightTool ) );

	[Shortcut( "tools.subterrain.noise", "8", typeof( SceneViewWidget ) )]
	public void ActivateNoise() => EditorToolManager.SetSubTool( nameof( SubNoiseTool ) );

	[Shortcut( "tools.subterrain.hole", "9", typeof( SceneViewWidget ) )]
	public void ActivateHole() => EditorToolManager.SetSubTool( nameof( SubHoleTool ) );

	[Shortcut( "tools.subterrain.plainer", "0", typeof( SceneViewWidget ) )]
	public void ActivatePlainer() => EditorToolManager.SetSubTool( nameof( SubPlainerTool ) );

	[Shortcut( "tools.subterrain.bridge", "B", typeof( SceneViewWidget ) )]
	public void ActivateBridge() => EditorToolManager.SetSubTool( nameof( SubBridgeTool ) );

	[Shortcut( "tools.subterrain.paint-ground", "CTRL+1", typeof( SceneViewWidget ) )]
	public void PaintGround() => SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Base );

	[Shortcut( "tools.subterrain.paint-wear", "CTRL+2", typeof( SceneViewWidget ) )]
	public void PaintWear() => SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Overlay );

	[Shortcut( "tools.subterrain.paint-path", "CTRL+3", typeof( SceneViewWidget ) )]
	public void PaintPath() => SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Path );

	[Shortcut( "tools.subterrain.brush-smaller", "CTRL+[", typeof( SceneViewWidget ) )]
	public void BrushSmaller() => SubTerrainTool.ActiveInstance?.NudgeBrushSize( -1 );

	[Shortcut( "tools.subterrain.brush-larger", "CTRL+]", typeof( SceneViewWidget ) )]
	public void BrushLarger() => SubTerrainTool.ActiveInstance?.NudgeBrushSize( 1 );

	[Shortcut( "tools.subterrain.brush-smaller-arrow", "LEFT", typeof( SceneViewWidget ) )]
	public void BrushSmallerArrow()
	{
		if ( SubTerrainTool.ActiveInstance is not null )
		{
			SubTerrainTool.ActiveInstance.NudgeBrushSize( -1 );
			return;
		}

		EditorShortcuts.PassShortcut = true;
	}

	[Shortcut( "tools.subterrain.brush-larger-arrow", "RIGHT", typeof( SceneViewWidget ) )]
	public void BrushLargerArrow()
	{
		if ( SubTerrainTool.ActiveInstance is not null )
		{
			SubTerrainTool.ActiveInstance.NudgeBrushSize( 1 );
			return;
		}

		EditorShortcuts.PassShortcut = true;
	}

	[Shortcut( "tools.subterrain.paint-tmat-prev", "UP", typeof( SceneViewWidget ) )]
	public void ArrowUp()
	{
		var tool = SubTerrainTool.ActiveInstance;
		if ( tool is null )
		{
			EditorShortcuts.PassShortcut = true;
			return;
		}

		if ( tool.PaintArmed )
		{
			if ( tool.NudgePaintMaterial( -1 ) )
				return;

			EditorShortcuts.PassShortcut = true;
			return;
		}

		tool.NudgeBrushOpacity( 1 );
	}

	[Shortcut( "tools.subterrain.paint-tmat-next", "DOWN", typeof( SceneViewWidget ) )]
	public void ArrowDown()
	{
		var tool = SubTerrainTool.ActiveInstance;
		if ( tool is null )
		{
			EditorShortcuts.PassShortcut = true;
			return;
		}

		if ( tool.PaintArmed )
		{
			if ( tool.NudgePaintMaterial( 1 ) )
				return;

			EditorShortcuts.PassShortcut = true;
			return;
		}

		tool.NudgeBrushOpacity( -1 );
	}
}
subzerostudios.subterrain-tool / Editor/SubTerrainBrushSizeWidget.cs
Editor library
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using Sandbox;

namespace Editor.SubTerrain;

/// <summary>
/// Brush Size row: piecewise track (fine under 3500, mid to 18000, coarse to 35000) plus type-in.
/// </summary>
internal sealed class SubTerrainBrushSizeWidget : Widget
{
	readonly SubTerrainTool _tool;
	readonly SubTerrainBrushSizeTrack _track;
	readonly LineEdit _field;
	bool _syncing;

	public SubTerrainBrushSizeWidget( SubTerrainTool tool, Widget parent ) : base( parent )
	{
		_tool = tool;

		Layout = Layout.Row();
		Layout.Spacing = 6;
		Layout.Margin = new Sandbox.UI.Margin( 0, 2, 10, 2 );

		var label = Layout.Add( new Label( "Size" ) );
		label.MinimumWidth = 72;
		label.Color = Theme.TextControl;
		label.ToolTip = "World units. Fine under 3500 (step 50), mid to 18000 (step 500), then to 35000 (step 1000). Left / Right or Ctrl+[ / ] steps.";

		_track = new SubTerrainBrushSizeTrack( this );
		Layout.Add( _track, 1 );

		_field = new LineEdit();
		_field.MinimumWidth = 52;
		_field.MaximumWidth = 56;
		_field.ToolTip = "Type an exact size (8-35000).";
		_field.EditingFinished += OnFieldFinished;
		Layout.Add( _field );

		FixedHeight = 28;
		MinimumHeight = 28;
		MaximumHeight = 28;

		SyncFromSettings();
	}

	public void SyncFromSettings()
	{
		if ( _tool?.BrushSettings is null )
			return;

		_syncing = true;
		int size = SubTerrainBrushSize.Clamp( _tool.BrushSettings.Size );
		_tool.BrushSettings.Size = size;
		_field.Text = size.ToString();
		_track.Update();
		_syncing = false;
	}

	public void SetSizeFromTrack( float t01 )
	{
		if ( _syncing || _tool?.BrushSettings is null )
			return;

		int size = SubTerrainBrushSize.SizeFromTrack( t01 );
		if ( size == _tool.BrushSettings.Size )
		{
			_track.Update();
			return;
		}

		_tool.BrushSettings.Size = size;
		_syncing = true;
		_field.Text = size.ToString();
		_syncing = false;
		_track.Update();
	}

	void OnFieldFinished()
	{
		if ( _syncing || _tool?.BrushSettings is null )
			return;

		if ( !int.TryParse( _field.Text.Trim(), out int parsed ) )
		{
			SyncFromSettings();
			return;
		}

		_tool.BrushSettings.Size = SubTerrainBrushSize.Clamp( parsed );
		SyncFromSettings();
	}
}

internal sealed class SubTerrainBrushSizeTrack : Widget
{
	readonly SubTerrainBrushSizeWidget _host;
	bool _dragging;

	public SubTerrainBrushSizeTrack( SubTerrainBrushSizeWidget host ) : base( host )
	{
		_host = host;
		MouseTracking = true;
		Cursor = CursorShape.Finger;
		MinimumHeight = 22;
		MaximumHeight = 22;
		ToolTip = "Drag: fine under 3500, mid to 18000, coarse to 35000. Left / Right or Ctrl+[ / ] use the same steps.";
	}

	protected override void OnMousePress( MouseEvent e )
	{
		if ( e.Button == MouseButtons.Left )
		{
			_dragging = true;
			ApplyAt( e.LocalPosition.x );
			e.Accepted = true;
			return;
		}

		base.OnMousePress( e );
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		if ( _dragging )
			ApplyAt( e.LocalPosition.x );
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		if ( e.Button == MouseButtons.Left )
			_dragging = false;

		base.OnMouseReleased( e );
	}

	void ApplyAt( float x )
	{
		float w = Math.Max( Width - 8f, 1f );
		float t = Math.Clamp( (x - 4f) / w, 0f, 1f );
		_host.SetSizeFromTrack( t );
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		Paint.Antialiasing = true;
		Paint.ClearPen();

		var track = new Rect( 4, Height * 0.5f - 2f, Math.Max( Width - 8f, 1f ), 4f );
		Paint.SetBrush( Color.White.WithAlpha( 0.08f ) );
		Paint.DrawRect( track, 2 );

		// Piecewise band ticks (fine / mid).
		float fineX = track.Left + track.Width * SubTerrainBrushSize.FineTrack;
		float midX = track.Left + track.Width * SubTerrainBrushSize.MidTrack;
		Paint.SetBrush( Color.White.WithAlpha( 0.14f ) );
		Paint.DrawRect( new Rect( fineX, track.Top - 2f, 1f, track.Height + 4f ) );
		Paint.DrawRect( new Rect( midX, track.Top - 2f, 1f, track.Height + 4f ) );

		var tool = SubTerrainTool.ActiveInstance;
		int size = tool?.BrushSettings is not null
			? SubTerrainBrushSize.Clamp( tool.BrushSettings.Size )
			: SubTerrainBrushSize.Min;

		float t = SubTerrainBrushSize.TrackFromSize( size );
		float thumbX = track.Left + track.Width * t;
		float thumbR = 6f;
		Paint.SetBrush( Theme.Primary );
		Paint.DrawCircle( new Vector2( thumbX, Height * 0.5f ), thumbR );
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 33,
    "Files": [
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/SubTerrainSculptMode.cs",
            "FileName": "SubTerrainSculptMode.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// Sculpt modes for the SubTerrain compute shader.\r\n/// Values 0-4 match the engine shader; 5-8 are SubTerrain extensions.\r\n/// </summary>\r\npublic enum SubTerrainSculptMode\r\n{\r\n\tRaiseLower = 0,\r\n\tFlatten = 1,\r\n\tSmooth = 2,\r\n\tHole = 3,\r\n\tNoise = 4,\r\n\tSlope = 5,\r\n\tPath = 6,\r\n\tPlainer = 7,\r\n\tBridge = 8\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"SubTerrain Tool\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"subterrain-tool\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"subzerostudios\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"subzerostudios.subterrain-tool\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-08-24T22:24:58.1339886Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.165.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.165.0\")]"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Code/TerrainEditRequest.cs",
            "FileName": "TerrainEditRequest.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\npublic struct TerrainEditRequest\r\n{\r\n\tpublic TerrainEditKind Kind;\r\n\tpublic Vector3 WorldPos;\r\n\tpublic float Radius;\r\n\r\n\t/// <summary>0 = kind auto-scale (radius \u00d7 DepthScale, clamped).</summary>\r\n\tpublic float Depth;\r\n\r\n\t/// <summary>Unused this pass. Reserved for a later trench (capsule SDF).</summary>\r\n\tpublic Vector3 EndPos;\r\n\r\n\tpublic bool HasTargetZ;\r\n\tpublic float TargetZ;\r\n\tpublic int Seed;\r\n}\r\n\r\npublic struct TerrainEditResult\r\n{\r\n\tpublic TerrainEditKind Kind;\r\n\tpublic Vector3 Origin;\r\n\tpublic float Radius;\r\n\tpublic float Depth;\r\n\tpublic RectInt Dirty;\r\n\tpublic int Texels;\r\n\tpublic float VolumeRemoved;\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/SubTerrainBlendWidget.cs",
            "FileName": "SubTerrainBlendWidget.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing Sandbox;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// Paint Texture sidebar: tmat list, Ground / Wear / Path, Steep / All, live mix.\r\n/// </summary>\r\ninternal class SubTerrainBlendWidget : Widget\r\n{\r\n\treadonly SubTerrainTool _tool;\r\n\treadonly SubTerrainPaintModeStrip _modes;\r\n\treadonly SubTerrainPairPreview _pairPreview;\r\n\treadonly Widget _listHost;\r\n\treadonly Widget _optionsHost;\r\n\treadonly Label _live;\r\n\tWidget _limitRow;\r\n\tCheckbox _keepPairBox;\r\n\tCheckbox _sampledBox;\r\n\tCheckbox _packCenterBox;\r\n\tint _listedCount = -1;\r\n\tSubTerrainPaintLayer _sheetLayer = (SubTerrainPaintLayer)(-1);\r\n\tbool _sheetSteep;\r\n\r\n\tconst string IntentSuffix = \" Alt sample | Ctrl fade | Shift smooth | ring yellow/green = Alt/Shift.\";\r\n\r\n\tpublic SubTerrainBlendWidget( SubTerrainTool tool, Widget parent ) : base( parent )\r\n\t{\r\n\t\t_tool = tool;\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Spacing = 4;\r\n\r\n\t\t_modes = Layout.Add( new SubTerrainPaintModeStrip( tool, this ) );\r\n\t\t_pairPreview = Layout.Add( new SubTerrainPairPreview( tool, this ) );\r\n\t\tSyncPairPreview();\r\n\t\t_listHost = Layout.Add( new Widget( this ) );\r\n\t\t_listHost.Layout = Layout.Column();\r\n\t\t_listHost.Layout.Spacing = 0;\r\n\t\t_optionsHost = Layout.Add( new Widget( this ) );\r\n\t\t_optionsHost.Layout = Layout.Column();\r\n\t\t_optionsHost.Layout.Spacing = 4;\r\n\t\t_live = Layout.Add( new Label( \"\" ) );\r\n\r\n\t\tRebuildList();\r\n\t\tRebuildOptions();\r\n\t\tRefreshCopy();\r\n\t}\r\n\r\n\tvoid OnGroundClicked() => SetLayer( SubTerrainPaintLayer.Base );\r\n\r\n\tvoid OnWearClicked() => SetLayer( SubTerrainPaintLayer.Overlay );\r\n\r\n\tvoid OnPathClicked()\r\n\t{\r\n\t\tvar terrain = _tool.PeekTerrain();\r\n\t\tint ground = SubTerrainSplatUtil.FindPathGround( terrain, _tool.PaintSettings );\r\n\t\tint wear = _tool.PaintSettings.MaterialIndex;\r\n\t\tif ( wear == ground )\r\n\t\t{\r\n\t\t\twear = SubTerrainSplatUtil.FindPathWear( terrain, ground );\r\n\t\t\t_tool.PaintSettings.MaterialIndex = wear;\r\n\t\t\tSubTerrainPaintTool.TrySetSplatChannel( wear );\r\n\t\t}\r\n\r\n\t\tSetLayer( SubTerrainPaintLayer.Path );\r\n\t}\r\n\r\n\tvoid SetLayer( SubTerrainPaintLayer layer )\r\n\t{\r\n\t\tSubTerrainPaintStore.Capture( _tool.PaintSettings );\r\n\t\tSubTerrainPaintStore.Apply( _tool.PaintSettings, layer );\r\n\t\tSubTerrainPaintTool.ActiveLayer = layer;\r\n\t\tRebuildOptions();\r\n\t\t_tool.RefreshPaintChrome();\r\n\t\tRefreshCopy();\r\n\t\t_modes.Update();\r\n\t\tSyncPairPreview();\r\n\t}\r\n\r\n\tvoid SyncPairPreview()\r\n\t{\r\n\t\tif ( _pairPreview is null )\r\n\t\t\treturn;\r\n\r\n\t\t// Ground writes one tmat - a pair thumb is dead chrome.\r\n\t\tbool show = _tool.PaintSettings.Layer != SubTerrainPaintLayer.Base;\r\n\t\t_pairPreview.Visible = show;\r\n\t\t_pairPreview.FixedHeight = show ? 36 : 0;\r\n\t\t_pairPreview.MinimumHeight = show ? 36 : 0;\r\n\t\t_pairPreview.MaximumHeight = show ? 36 : 0;\r\n\t\tif ( show )\r\n\t\t\t_pairPreview.Update();\r\n\t}\r\n\r\n\tinternal void SelectGround() => OnGroundClicked();\r\n\tinternal void SelectWear() => OnWearClicked();\r\n\tinternal void SelectPath() => OnPathClicked();\r\n\r\n\tinternal void OnMaterialSelectionChanged()\r\n\t{\r\n\t\tRefreshCopy();\r\n\t\tSyncPairPreview();\r\n\t\t_listHost?.Update();\r\n\t\tif ( _listHost?.Layout is null )\r\n\t\t\treturn;\r\n\r\n\t\tforeach ( var child in _listHost.Children )\r\n\t\t\tchild?.Update();\r\n\t}\r\n\r\n\tinternal void SelectSteep()\r\n\t{\r\n\t\t_tool.PaintSettings.OnlyOnSlope = true;\r\n\t\tRebuildOptions();\r\n\t\tRefreshCopy();\r\n\t}\r\n\r\n\tinternal void SelectAll()\r\n\t{\r\n\t\t_tool.PaintSettings.OnlyOnSlope = false;\r\n\t\tRebuildOptions();\r\n\t\tRefreshCopy();\r\n\t}\r\n\r\n\tvoid OnKeepPairToggled()\r\n\t{\r\n\t\tif ( _keepPairBox is null )\r\n\t\t\treturn;\r\n\r\n\t\t_tool.PaintSettings.MixOnly = _keepPairBox.Value;\r\n\t\tRefreshCopy();\r\n\t}\r\n\r\n\tvoid OnSampledToggled()\r\n\t{\r\n\t\tif ( _sampledBox is null )\r\n\t\t\treturn;\r\n\r\n\t\t_tool.PaintSettings.LimitToSampledGround = _sampledBox.Value;\r\n\t\tRefreshCopy();\r\n\t}\r\n\r\n\tvoid OnPackCenterToggled()\r\n\t{\r\n\t\tif ( _packCenterBox is null )\r\n\t\t\treturn;\r\n\r\n\t\t_tool.PaintSettings.PackCenter = _packCenterBox.Value;\r\n\t\tRebuildOptions();\r\n\t\tRefreshCopy();\r\n\t\tSyncPairPreview();\r\n\t}\r\n\r\n\tvoid RebuildList()\r\n\t{\r\n\t\tif ( _listHost is null )\r\n\t\t\treturn;\r\n\r\n\t\t_listHost.Layout ??= Layout.Column();\r\n\t\t_listHost.Layout.Clear( true );\r\n\r\n\t\tvar terrain = _tool.PeekTerrain();\r\n\t\tvar materials = terrain?.Storage?.Materials;\r\n\t\tint count = materials?.Count ?? 0;\r\n\t\t_listedCount = count;\r\n\r\n\t\tif ( count == 0 )\r\n\t\t{\r\n\t\t\t_listHost.Layout.Add( new Label( \"This terrain has no materials to paint.\" ) );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfor ( int i = 0; i < count; i++ )\r\n\t\t{\r\n\t\t\tvar name = SubTerrainSplatUtil.MaterialName( terrain, i );\r\n\t\t\t_listHost.Layout.Add( new SubTerrainMaterialRow( _tool, i, name, _listHost ) );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid RebuildOptions()\r\n\t{\r\n\t\tif ( _optionsHost is null )\r\n\t\t\treturn;\r\n\r\n\t\t_optionsHost.Layout ??= Layout.Column();\r\n\t\t_optionsHost.Layout.Clear( true );\r\n\t\t_limitRow = null;\r\n\t\t_keepPairBox = null;\r\n\t\t_sampledBox = null;\r\n\t\t_packCenterBox = null;\r\n\t\t_sheetLayer = _tool.PaintSettings.Layer;\r\n\t\t_sheetSteep = _tool.PaintSettings.OnlyOnSlope;\r\n\r\n\t\tvar so = _tool.PaintSettings.GetSerialized();\r\n\t\tif ( so is null )\r\n\t\t\treturn;\r\n\r\n\t\tif ( _sheetLayer == SubTerrainPaintLayer.Overlay )\r\n\t\t\t_optionsHost.Layout.Add( new SubTerrainWhereStrip( _tool, this ) );\r\n\r\n\t\tvar sheet = new ControlSheet { IncludePropertyNames = false };\r\n\t\tif ( _sheetLayer == SubTerrainPaintLayer.Overlay )\r\n\t\t\tAddSettingRow( sheet, so, nameof( SubTerrainPaintSettings.HowWorn ) );\r\n\t\telse if ( _sheetLayer == SubTerrainPaintLayer.Path )\r\n\t\t\tAddSettingRow( sheet, so, nameof( SubTerrainPaintSettings.CenterMix ) );\r\n\r\n\t\tif ( _sheetLayer == SubTerrainPaintLayer.Overlay && _sheetSteep )\r\n\t\t\tAddSettingRow( sheet, so, nameof( SubTerrainPaintSettings.SlopeDegrees ) );\r\n\r\n\t\t_optionsHost.Layout.Add( sheet );\r\n\r\n\t\tif ( _sheetLayer == SubTerrainPaintLayer.Overlay )\r\n\t\t{\r\n\t\t\t_keepPairBox = new Checkbox( \"Keep pair\" );\r\n\t\t\t_keepPairBox.Value = _tool.PaintSettings.MixOnly;\r\n\t\t\t_keepPairBox.ToolTip = \"Only move mix. Do not change the base/overlay pair (avoids texel stairs next to a different pair).\";\r\n\t\t\t_keepPairBox.Toggled += OnKeepPairToggled;\r\n\t\t\t_optionsHost.Layout.Add( _keepPairBox );\r\n\r\n\t\t\t_sampledBox = new Checkbox( \"Stay on sampled\" );\r\n\t\t\t_sampledBox.Value = _tool.PaintSettings.LimitToSampledGround;\r\n\t\t\t_sampledBox.ToolTip = \"Alt-click locks to that ground base. Wear will not paint other bases.\";\r\n\t\t\t_sampledBox.Toggled += OnSampledToggled;\r\n\t\t\t_optionsHost.Layout.Add( _sampledBox );\r\n\t\t\t_limitRow = _sampledBox;\r\n\t\t}\r\n\t\telse if ( _sheetLayer == SubTerrainPaintLayer.Path )\r\n\t\t{\r\n\t\t\t_packCenterBox = new Checkbox( \"Pack center\" );\r\n\t\t\t_packCenterBox.Value = _tool.PaintSettings.PackCenter;\r\n\t\t\t_packCenterBox.ToolTip = \"On: solid path core along the stroke, soft apron outside. Off: soft ground + path apron only.\";\r\n\t\t\t_packCenterBox.Toggled += OnPackCenterToggled;\r\n\t\t\t_optionsHost.Layout.Add( _packCenterBox );\r\n\r\n\t\t\tif ( _tool.PaintSettings.PackCenter )\r\n\t\t\t{\r\n\t\t\t\tvar coreSheet = new ControlSheet { IncludePropertyNames = false };\r\n\t\t\t\tAddSettingRow( coreSheet, so, nameof( SubTerrainPaintSettings.PackCore ) );\r\n\t\t\t\t_optionsHost.Layout.Add( coreSheet );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void AddSettingRow( ControlSheet sheet, SerializedObject so, string name )\r\n\t{\r\n\t\tif ( so.TryGetProperty( name, out var prop ) && prop is not null )\r\n\t\t\tsheet.AddRow( prop );\r\n\t}\r\n\r\n\tvoid RefreshCopy()\r\n\t{\r\n\t\tif ( _live is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar line = WriteLine();\r\n\t\tif ( _tool.PaintSettings.Layer == SubTerrainPaintLayer.Overlay && _tool.PaintSettings.Dithers )\r\n\t\t\tline += \" | dither\";\r\n\r\n\t\t_live.Text = line;\r\n\t\t_live.ToolTip = IntentTip();\r\n\t\t_live.Color = Theme.TextControl;\r\n\t}\r\n\r\n\tstring WriteLine()\r\n\t{\r\n\t\tvar settings = _tool.PaintSettings;\r\n\t\tvar layer = settings.Layer;\r\n\t\tvar terrain = _tool.PeekTerrain();\r\n\t\tstring mix = settings.MixCap.ToString( \"0.00\" );\r\n\r\n\t\tif ( layer == SubTerrainPaintLayer.Path )\r\n\t\t{\r\n\t\t\tint ground = SubTerrainSplatUtil.FindPathGround( terrain, settings );\r\n\t\t\tint wear = settings.MaterialIndex;\r\n\t\t\tif ( ground == wear )\r\n\t\t\t\treturn \"Click the packed tmat. Alt-click the ground.\";\r\n\r\n\t\t\tvar gName = SubTerrainSplatUtil.MaterialName( terrain, ground );\r\n\t\t\tvar wName = SubTerrainSplatUtil.MaterialName( terrain, wear );\r\n\t\t\treturn settings.PackCenter\r\n\t\t\t\t? $\"{wName} center | {gName} | {wName} {mix} | core {settings.PackCore:0.00}\"\r\n\t\t\t\t: $\"{gName} | {wName} {mix} | soft\";\r\n\t\t}\r\n\r\n\t\tif ( layer == SubTerrainPaintLayer.Base )\r\n\t\t\treturn SubTerrainSplatUtil.MaterialName( terrain, settings.MaterialIndex );\r\n\r\n\t\tvar name = SubTerrainSplatUtil.MaterialName( terrain, settings.MaterialIndex );\r\n\t\tstring lockOn = \"\";\r\n\t\tif ( settings.LimitToSampledGround && settings.HasSampledGround )\r\n\t\t\tlockOn = $\" on {SubTerrainSplatUtil.MaterialName( terrain, settings.SampledGroundIndex )}\";\r\n\r\n\t\tif ( settings.MixOnly )\r\n\t\t\treturn $\"mix {name} {mix}{lockOn}\";\r\n\r\n\t\treturn settings.OnlyOnSlope\r\n\t\t\t? $\"{name} {mix} steep{lockOn}\"\r\n\t\t\t: $\"{name} {mix}{lockOn}\";\r\n\t}\r\n\r\n\tstring IntentTip()\r\n\t{\r\n\t\tvar settings = _tool.PaintSettings;\r\n\t\tstring body = settings.Layer switch\r\n\t\t{\r\n\t\t\tSubTerrainPaintLayer.Path => settings.PackCenter\r\n\t\t\t\t? \"Pack center on. Raise Core for a wider solid middle. Center mix is the soft apron. Brush Size widens the whole blend.\"\r\n\t\t\t\t: \"Pack center off. Soft apron only: ground + path. Center mix is the apron mix.\",\r\n\t\t\tSubTerrainPaintLayer.Base => \"Ground writes the selected tmat as base and overlay, mix 0.\",\r\n\t\t\t_ => \"Wear writes overlay mix. Steep skips flats. Ctrl reduces. Alt-click locks Stay on sampled.\"\r\n\t\t};\r\n\r\n\t\treturn body + IntentSuffix;\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void Refresh()\r\n\t{\r\n\t\tif ( _live is null || _modes is null || _listHost is null || _optionsHost is null )\r\n\t\t\treturn;\r\n\r\n\t\t_tool.RefreshPaintChrome();\r\n\r\n\t\tif ( !_tool.PaintArmed )\r\n\t\t\treturn;\r\n\r\n\t\tvar terrain = _tool.PeekTerrain();\r\n\t\tint count = terrain?.Storage?.Materials?.Count ?? 0;\r\n\t\tif ( count != _listedCount )\r\n\t\t\tRebuildList();\r\n\r\n\t\tvar settings = _tool.PaintSettings;\r\n\t\tif ( _sheetLayer != settings.Layer || _sheetSteep != settings.OnlyOnSlope )\r\n\t\t\tRebuildOptions();\r\n\r\n\t\tif ( _keepPairBox is not null )\r\n\t\t\t_keepPairBox.Value = settings.MixOnly;\r\n\r\n\t\tif ( _sampledBox is not null )\r\n\t\t{\r\n\t\t\t_sampledBox.Value = settings.LimitToSampledGround;\r\n\t\t\t_sampledBox.Enabled = settings.HasSampledGround;\r\n\t\t\tif ( !settings.HasSampledGround )\r\n\t\t\t\t_sampledBox.ToolTip = \"Alt-click the ground to lock Stay on sampled.\";\r\n\t\t\telse\r\n\t\t\t\t_sampledBox.ToolTip = \"Locked to the Alt-clicked ground base. Uncheck to paint any base.\";\r\n\t\t}\r\n\r\n\t\tif ( _packCenterBox is not null )\r\n\t\t\t_packCenterBox.Value = settings.PackCenter;\r\n\r\n\t\tif ( _limitRow is not null )\r\n\t\t\t_limitRow.Enabled = settings.HasSampledGround;\r\n\r\n\t\tRefreshCopy();\r\n\t\t_modes.Update();\r\n\t\tSyncPairPreview();\r\n\r\n\t\tif ( !_tool.StatusValid || terrain is null || !terrain.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tif ( !SubTerrainSplatUtil.TryTexel( terrain, _tool.StatusHitLocal, out var x, out var y ) )\r\n\t\t\treturn;\r\n\r\n\t\tvar mat = SubTerrainSplatUtil.Read( terrain, x, y );\r\n\t\tif ( mat.IsHole )\r\n\t\t{\r\n\t\t\t_live.Text = WriteLine() + \" | hole\";\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar baseName = SubTerrainSplatUtil.MaterialName( terrain, mat.BaseTextureId );\r\n\t\tvar overlayName = SubTerrainSplatUtil.MaterialName( terrain, mat.OverlayTextureId );\r\n\t\tfloat mix = mat.BlendFactor / 255f;\r\n\t\tstring there = baseName == overlayName || mix < 0.02f\r\n\t\t\t? baseName\r\n\t\t\t: $\"{baseName} + {overlayName} {mix:0.00}\";\r\n\r\n\t\tstring extra = \"\";\r\n\t\tif ( SubTerrainSplatUtil.TrySlopeDegrees( terrain, x, y, out var degrees ) )\r\n\t\t{\r\n\t\t\tint deg = (int)MathF.Round( degrees );\r\n\t\t\tbool steepGate = settings.Layer == SubTerrainPaintLayer.Overlay && settings.OnlyOnSlope;\r\n\t\t\tif ( steepGate )\r\n\t\t\t\textra = degrees >= settings.SlopeDegrees ? $\" | steep {deg}\" : $\" | skip {deg}\";\r\n\t\t}\r\n\r\n\t\tif ( SubTerrainSplatUtil.HasSeam( terrain, x, y ) )\r\n\t\t\textra += \" | seam\";\r\n\r\n\t\t_live.Text = $\"{WriteLine()} | {there}{extra}\";\r\n\t}\r\n}\r\n\r\ninternal class SubTerrainPairPreview : Widget\r\n{\r\n\treadonly SubTerrainTool _tool;\r\n\tconst float ThumbSize = 24f;\r\n\r\n\tpublic SubTerrainPairPreview( SubTerrainTool tool, Widget parent ) : base( parent )\r\n\t{\r\n\t\t_tool = tool;\r\n\t\tFixedHeight = 36;\r\n\t\tMinimumHeight = 36;\r\n\t\tMaximumHeight = 36;\r\n\t\tToolTip = \"Pair this stroke will write.\";\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tif ( !Visible || _tool.PaintSettings.Layer == SubTerrainPaintLayer.Base )\r\n\t\t\treturn;\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Color.White.WithAlpha( 0.04f ) );\r\n\t\tPaint.DrawRect( LocalRect );\r\n\r\n\t\tResolvePair( out int left, out int right );\r\n\t\tfloat y = (Height - ThumbSize) * 0.5f;\r\n\t\tfloat gap = 8f;\r\n\t\tfloat total = ThumbSize * 2f + gap;\r\n\t\tfloat x0 = (Width - total) * 0.5f;\r\n\r\n\t\tDrawThumb( new Rect( x0, y, ThumbSize, ThumbSize ), left );\r\n\t\tDrawThumb( new Rect( x0 + ThumbSize + gap, y, ThumbSize, ThumbSize ), right );\r\n\t}\r\n\r\n\tvoid ResolvePair( out int left, out int right )\r\n\t{\r\n\t\tvar settings = _tool.PaintSettings;\r\n\t\tvar terrain = _tool.PeekTerrain();\r\n\t\tint selected = settings.MaterialIndex;\r\n\r\n\t\tif ( settings.Layer == SubTerrainPaintLayer.Path )\r\n\t\t{\r\n\t\t\tleft = SubTerrainSplatUtil.FindPathGround( terrain, settings );\r\n\t\t\tright = selected;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( settings.Layer == SubTerrainPaintLayer.Overlay )\r\n\t\t{\r\n\t\t\tleft = settings.HasSampledGround\r\n\t\t\t\t? settings.SampledGroundIndex\r\n\t\t\t\t: HoverBaseOr( terrain, selected );\r\n\t\t\tright = selected;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tleft = selected;\r\n\t\tright = selected;\r\n\t}\r\n\r\n\tint HoverBaseOr( Terrain terrain, int fallback )\r\n\t{\r\n\t\tif ( terrain is null || !_tool.StatusValid )\r\n\t\t\treturn fallback;\r\n\r\n\t\tif ( !SubTerrainSplatUtil.TryTexel( terrain, _tool.StatusHitLocal, out var x, out var y ) )\r\n\t\t\treturn fallback;\r\n\r\n\t\tvar mat = SubTerrainSplatUtil.Read( terrain, x, y );\r\n\t\tif ( mat.IsHole )\r\n\t\t\treturn fallback;\r\n\r\n\t\treturn mat.BaseTextureId;\r\n\t}\r\n\r\n\tvoid DrawThumb( Rect rect, int index )\r\n\t{\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Theme.SurfaceBackground.WithAlpha( 0.35f ) );\r\n\t\tPaint.DrawRect( rect, 3 );\r\n\r\n\t\tvar path = MaterialPath( index );\r\n\t\tvar asset = string.IsNullOrWhiteSpace( path ) ? null : AssetSystem.FindByPath( path );\r\n\t\tif ( asset is null || asset.IsDeleted )\r\n\t\t\treturn;\r\n\r\n\t\tvar thumb = asset.GetAssetThumb( true );\r\n\t\tif ( thumb is not null )\r\n\t\t\tPaint.Draw( rect, thumb );\r\n\t}\r\n\r\n\tstring MaterialPath( int index )\r\n\t{\r\n\t\tvar mats = _tool.PeekTerrain()?.Storage?.Materials;\r\n\t\tif ( mats is null || index < 0 || index >= mats.Count )\r\n\t\t\treturn null;\r\n\r\n\t\treturn mats[index]?.ResourcePath;\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void TickPreview()\r\n\t{\r\n\t\tif ( !Visible || _tool.PaintSettings.Layer == SubTerrainPaintLayer.Base )\r\n\t\t\treturn;\r\n\r\n\t\tUpdate();\r\n\t}\r\n}\r\n\r\ninternal class SubTerrainPaintModeStrip : Widget\r\n{\r\n\treadonly SubTerrainTool _tool;\r\n\treadonly SubTerrainBlendWidget _host;\r\n\tconst int SlotCount = 3;\r\n\tint _hover = -1;\r\n\r\n\tpublic SubTerrainPaintModeStrip( SubTerrainTool tool, SubTerrainBlendWidget host ) : base( host )\r\n\t{\r\n\t\t_tool = tool;\r\n\t\t_host = host;\r\n\t\tFixedHeight = 28;\r\n\t\tMinimumHeight = 28;\r\n\t\tMaximumHeight = 28;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tMouseTracking = true;\r\n\t\tToolTip = \"Ground is a solid tmat. Wear is overlay mix. Path packs a path tmat (optional Pack center) with a ground+path apron.\";\r\n\t}\r\n\r\n\tprotected override void OnMouseClick( MouseEvent e )\r\n\t{\r\n\t\tint slot = SlotAt( e.LocalPosition.x );\r\n\t\tif ( slot == 0 )\r\n\t\t\t_host.SelectGround();\r\n\t\telse if ( slot == 1 )\r\n\t\t\t_host.SelectWear();\r\n\t\telse\r\n\t\t\t_host.SelectPath();\r\n\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseMove( e );\r\n\t\tint next = SlotAt( e.LocalPosition.x );\r\n\t\tif ( next == _hover )\r\n\t\t\treturn;\r\n\r\n\t\t_hover = next;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMouseLeave()\r\n\t{\r\n\t\t_hover = -1;\r\n\t\tUpdate();\r\n\t\tbase.OnMouseLeave();\r\n\t}\r\n\r\n\tint SlotAt( float x )\r\n\t{\r\n\t\tfloat w = Math.Max( Width, 1f );\r\n\t\tint slot = (int)(x / w * SlotCount);\r\n\t\treturn Math.Clamp( slot, 0, SlotCount - 1 );\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Color.White.WithAlpha( 0.04f ) );\r\n\t\tPaint.DrawRect( LocalRect );\r\n\r\n\t\tvar layer = _tool.PaintSettings.Layer;\r\n\t\tfloat slotW = Width / SlotCount;\r\n\t\tfor ( int i = 0; i < SlotCount; i++ )\r\n\t\t{\r\n\t\t\tvar rect = new Rect( i * slotW, 0, slotW, Height );\r\n\t\t\tbool selected = i switch\r\n\t\t\t{\r\n\t\t\t\t0 => layer == SubTerrainPaintLayer.Base,\r\n\t\t\t\t1 => layer == SubTerrainPaintLayer.Overlay,\r\n\t\t\t\t_ => layer == SubTerrainPaintLayer.Path\r\n\t\t\t};\r\n\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tif ( selected )\r\n\t\t\t\tPaint.SetBrush( Theme.Primary.WithAlpha( 0.20f ) );\r\n\t\t\telse if ( _hover == i )\r\n\t\t\t\tPaint.SetBrush( Color.White.WithAlpha( 0.06f ) );\r\n\t\t\telse\r\n\t\t\t\tPaint.SetBrush( Color.Transparent );\r\n\r\n\t\t\tPaint.DrawRect( rect );\r\n\r\n\t\t\tif ( i > 0 )\r\n\t\t\t{\r\n\t\t\t\tPaint.ClearPen();\r\n\t\t\t\tPaint.SetBrush( Color.White.WithAlpha( 0.10f ) );\r\n\t\t\t\tPaint.DrawRect( new Rect( rect.Left, rect.Top + 5, 1, rect.Height - 10 ) );\r\n\t\t\t}\r\n\r\n\t\t\tstring label = i switch\r\n\t\t\t{\r\n\t\t\t\t0 => \"Ground\",\r\n\t\t\t\t1 => \"Wear\",\r\n\t\t\t\t_ => \"Path\"\r\n\t\t\t};\r\n\r\n\t\t\tPaint.SetPen( selected ? Theme.Primary : Theme.TextControl );\r\n\t\t\tPaint.DrawText( rect, label, TextFlag.Center );\r\n\t\t}\r\n\t}\r\n}\r\n\r\ninternal class SubTerrainWhereStrip : Widget\r\n{\r\n\treadonly SubTerrainTool _tool;\r\n\treadonly SubTerrainBlendWidget _host;\r\n\tconst int SlotCount = 2;\r\n\tint _hover = -1;\r\n\r\n\tpublic SubTerrainWhereStrip( SubTerrainTool tool, SubTerrainBlendWidget host ) : base( host )\r\n\t{\r\n\t\t_tool = tool;\r\n\t\t_host = host;\r\n\t\tFixedHeight = 28;\r\n\t\tMinimumHeight = 28;\r\n\t\tMaximumHeight = 28;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tMouseTracking = true;\r\n\t\tToolTip = \"Steep skips ground that is too flat. All paints the whole brush, including shelves.\";\r\n\t}\r\n\r\n\tprotected override void OnMouseClick( MouseEvent e )\r\n\t{\r\n\t\tif ( SlotAt( e.LocalPosition.x ) == 0 )\r\n\t\t\t_host.SelectSteep();\r\n\t\telse\r\n\t\t\t_host.SelectAll();\r\n\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseMove( e );\r\n\t\tint next = SlotAt( e.LocalPosition.x );\r\n\t\tif ( next == _hover )\r\n\t\t\treturn;\r\n\r\n\t\t_hover = next;\r\n\t\tToolTip = next == 0\r\n\t\t\t? \"Skip ground that is too flat. Hover a hill; the footer says steep or skip.\"\r\n\t\t\t: \"Paint the whole brush, including shelves.\";\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMouseLeave()\r\n\t{\r\n\t\t_hover = -1;\r\n\t\tToolTip = \"Steep skips ground that is too flat. All paints the whole brush, including shelves.\";\r\n\t\tUpdate();\r\n\t\tbase.OnMouseLeave();\r\n\t}\r\n\r\n\tint SlotAt( float x )\r\n\t{\r\n\t\tfloat w = Math.Max( Width, 1f );\r\n\t\tint slot = (int)(x / w * SlotCount);\r\n\t\treturn Math.Clamp( slot, 0, SlotCount - 1 );\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Color.White.WithAlpha( 0.04f ) );\r\n\t\tPaint.DrawRect( LocalRect );\r\n\r\n\t\tbool steep = _tool.PaintSettings.OnlyOnSlope;\r\n\t\tfloat slotW = Width / SlotCount;\r\n\t\tfor ( int i = 0; i < SlotCount; i++ )\r\n\t\t{\r\n\t\t\tvar rect = new Rect( i * slotW, 0, slotW, Height );\r\n\t\t\tbool selected = i == 0 ? steep : !steep;\r\n\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tif ( selected )\r\n\t\t\t\tPaint.SetBrush( Theme.Primary.WithAlpha( 0.20f ) );\r\n\t\t\telse if ( _hover == i )\r\n\t\t\t\tPaint.SetBrush( Color.White.WithAlpha( 0.06f ) );\r\n\t\t\telse\r\n\t\t\t\tPaint.SetBrush( Color.Transparent );\r\n\r\n\t\t\tPaint.DrawRect( rect );\r\n\r\n\t\t\tif ( i > 0 )\r\n\t\t\t{\r\n\t\t\t\tPaint.ClearPen();\r\n\t\t\t\tPaint.SetBrush( Color.White.WithAlpha( 0.10f ) );\r\n\t\t\t\tPaint.DrawRect( new Rect( rect.Left, rect.Top + 5, 1, rect.Height - 10 ) );\r\n\t\t\t}\r\n\r\n\t\t\tPaint.SetPen( selected ? Theme.Primary : Theme.TextControl );\r\n\t\t\tPaint.DrawText( rect, i == 0 ? \"Steep\" : \"All\", TextFlag.Center );\r\n\t\t}\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void TickSelect()\r\n\t{\r\n\t\tUpdate();\r\n\t}\r\n}\r\n\r\ninternal class SubTerrainMaterialRow : Widget\r\n{\r\n\treadonly SubTerrainTool _tool;\r\n\treadonly int _index;\r\n\treadonly string _name;\r\n\treadonly string _resourcePath;\r\n\r\n\tconst float RowHeight = 42f;\r\n\tconst float ThumbSize = 28f;\r\n\r\n\tpublic SubTerrainMaterialRow( SubTerrainTool tool, int index, string name, Widget parent ) : base( parent )\r\n\t{\r\n\t\t_tool = tool;\r\n\t\t_index = index;\r\n\t\t_name = name;\r\n\t\t_resourcePath = tool.PeekTerrain()?.Storage?.Materials is { Count: > 0 } mats && index >= 0 && index < mats.Count\r\n\t\t\t? mats[index]?.ResourcePath\r\n\t\t\t: null;\r\n\t\tFixedHeight = RowHeight;\r\n\t\tMinimumHeight = RowHeight;\r\n\t\tMaximumHeight = RowHeight;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tToolTip = name;\r\n\t\tMouseTracking = true;\r\n\t}\r\n\r\n\tprotected override void OnMouseClick( MouseEvent e )\r\n\t{\r\n\t\tvar settings = _tool.PaintSettings;\r\n\t\tbool path = settings.Layer == SubTerrainPaintLayer.Path;\r\n\t\tbool alt = Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt );\r\n\r\n\t\tif ( path && alt )\r\n\t\t{\r\n\t\t\tif ( _index == settings.MaterialIndex )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tsettings.PathGroundIndex = _index;\r\n\t\t\tUpdate();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( path && _index == SubTerrainSplatUtil.FindPathGround( _tool.PeekTerrain(), settings ) )\r\n\t\t{\r\n\t\t\tsettings.PathGroundIndex = _index;\r\n\t\t\tUpdate();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tsettings.MaterialIndex = _index;\r\n\t\tSubTerrainPaintTool.TrySetSplatChannel( _index );\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnDoubleClick( MouseEvent e )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( _resourcePath ) )\r\n\t\t\treturn;\r\n\r\n\t\tvar asset = AssetSystem.FindByPath( _resourcePath );\r\n\t\tif ( asset is null || asset.IsDeleted )\r\n\t\t\treturn;\r\n\r\n\t\tasset.OpenInEditor();\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tvar settings = _tool.PaintSettings;\r\n\t\tbool selected = settings.MaterialIndex == _index;\r\n\t\tbool path = settings.Layer == SubTerrainPaintLayer.Path;\r\n\t\tbool pathGround = path && SubTerrainSplatUtil.FindPathGround( _tool.PeekTerrain(), settings ) == _index;\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\t\tif ( selected )\r\n\t\t\tPaint.SetBrush( Theme.Primary.WithAlpha( 0.18f ) );\r\n\t\telse if ( IsUnderMouse )\r\n\t\t\tPaint.SetBrush( Color.White.WithAlpha( 0.06f ) );\r\n\t\telse\r\n\t\t\tPaint.SetBrush( Color.Transparent );\r\n\r\n\t\tPaint.DrawRect( LocalRect );\r\n\r\n\t\tvar thumbRect = new Rect( 8, (RowHeight - ThumbSize) * 0.5f, ThumbSize, ThumbSize );\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Theme.SurfaceBackground.WithAlpha( 0.35f ) );\r\n\t\tPaint.DrawRect( thumbRect, 3 );\r\n\r\n\t\tvar asset = string.IsNullOrWhiteSpace( _resourcePath ) ? null : AssetSystem.FindByPath( _resourcePath );\r\n\t\tif ( asset is not null && !asset.IsDeleted )\r\n\t\t{\r\n\t\t\tvar thumb = asset.GetAssetThumb( true );\r\n\t\t\tif ( thumb is not null )\r\n\t\t\t\tPaint.Draw( thumbRect, thumb );\r\n\t\t}\r\n\r\n\t\tfloat textLeft = thumbRect.Right + 8f;\r\n\t\tPaint.SetPen( selected ? Color.White : Theme.TextControl );\r\n\t\tPaint.DrawText( new Rect( textLeft, 0, Math.Max( 40f, Width - textLeft - 56f ), Height ), _name, TextFlag.LeftCenter );\r\n\r\n\t\tstring role = null;\r\n\t\tif ( pathGround )\r\n\t\t\trole = \"ground\";\r\n\t\telse if ( path && selected )\r\n\t\t\trole = \"path\";\r\n\t\telse if ( !path && selected && settings.Layer == SubTerrainPaintLayer.Overlay )\r\n\t\t\trole = settings.MixCap.ToString( \"0.00\" );\r\n\r\n\t\tif ( role is not null )\r\n\t\t{\r\n\t\t\tPaint.SetPen( Color.White.WithAlpha( selected ? 0.70f : 0.45f ) );\r\n\t\t\tPaint.DrawText( new Rect( Width - 56, 0, 44, Height ), role, TextFlag.RightCenter );\r\n\t\t}\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void TickSelect()\r\n\t{\r\n\t\tbool path = _tool.PaintSettings.Layer == SubTerrainPaintLayer.Path;\r\n\t\tToolTip = path\r\n\t\t\t? \"Click the packed tmat. Alt-click the ground under it. Double-click opens the asset.\"\r\n\t\t\t: _name + \" | Double-click opens the asset.\";\r\n\t\tUpdate();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Code/TerrainSculptNet.cs",
            "FileName": "TerrainSculptNet.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\n/// <summary>\r\n/// Optional host-authoritative play-sculpt net. Attach to a NetworkMode.Object\r\n/// GameObject. Host writes height; peers apply the same op locally.\r\n/// </summary>\r\n[Title( \"Terrain Sculpt Net\" )]\r\n[Category( \"SubTerrain\" )]\r\n[Icon( \"share\" )]\r\npublic sealed class TerrainSculptNet : Component\r\n{\r\n\tpublic bool Submit( TerrainEditRequest request )\r\n\t{\r\n\t\tif ( request.Seed == 0 )\r\n\t\t\trequest.Seed = Game.Random.Int( 1, 1_000_000_000 );\r\n\r\n\t\tif ( !Networking.IsActive || Networking.IsHost )\r\n\t\t{\r\n\t\t\tApplyLocal( request );\r\n\t\t\tif ( Networking.IsActive )\r\n\t\t\t\tBroadcastStamp( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tRequestHost( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );\r\n\t\treturn true;\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tpublic void RequestHost( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )\r\n\t{\r\n\t\tvar request = Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ );\r\n\t\tApplyLocal( request );\r\n\t\tBroadcastStamp( kind, pos, radius, depth, seed, hasTargetZ, targetZ );\r\n\t}\r\n\r\n\t[Rpc.Broadcast]\r\n\tpublic void BroadcastStamp( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )\r\n\t{\r\n\t\tif ( Networking.IsHost )\r\n\t\t\treturn;\r\n\r\n\t\tApplyLocal( Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ ) );\r\n\t}\r\n\r\n\tstatic TerrainEditRequest Make( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )\r\n\t{\r\n\t\treturn new TerrainEditRequest\r\n\t\t{\r\n\t\t\tKind = (TerrainEditKind)kind,\r\n\t\t\tWorldPos = pos,\r\n\t\t\tRadius = radius,\r\n\t\t\tDepth = depth,\r\n\t\t\tSeed = seed,\r\n\t\t\tHasTargetZ = hasTargetZ,\r\n\t\t\tTargetZ = targetZ\r\n\t\t};\r\n\t}\r\n\r\n\tvoid ApplyLocal( TerrainEditRequest request )\r\n\t{\r\n\t\tTerrainSculpt.TryApply( Scene, request );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "TerrainBrushSettings.cs",
            "FileName": "TerrainBrushSettings.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\npublic sealed class CraterBrushSettings\r\n{\r\n\t[Property, Range( 16f, 200f )]\r\n\tpublic float MaxDepth { get; set; } = 96f;\r\n\r\n\t[Property, Title( \"Depth Scale\" ), Range( 0.1f, 1.5f )]\r\n\tpublic float DepthScale { get; set; } = 0.5f;\r\n\r\n\t[Property, Range( 0f, 32f )]\r\n\tpublic float MaxRaise { get; set; } = 12f;\r\n\r\n\t[Property, Range( 0.4f, 0.95f )]\r\n\tpublic float BowlRadius { get; set; } = 0.75f;\r\n\r\n\t[Property, Range( 0f, 0.7f )]\r\n\tpublic float Irregularity { get; set; } = 0.35f;\r\n\r\n\t[Property, Range( 0f, 0.5f )]\r\n\tpublic float Stretch { get; set; } = 0.25f;\r\n\r\n\t[Property, Range( 0f, 1f )]\r\n\tpublic float Blend { get; set; } = 0.65f;\r\n}\r\n\r\npublic sealed class DigBrushSettings\r\n{\r\n\t[Property, Range( 16f, 120f )]\r\n\tpublic float MaxDepth { get; set; } = 48f;\r\n\r\n\t[Property, Title( \"Depth Scale\" ), Range( 0.1f, 1.5f )]\r\n\tpublic float DepthScale { get; set; } = 0.85f;\r\n\r\n\t[Property, Range( 0f, 40f )]\r\n\tpublic float MaxRaise { get; set; } = 18f;\r\n\r\n\t[Property, Range( 0.7f, 0.98f )]\r\n\tpublic float BowlRadius { get; set; } = 0.9f;\r\n\r\n\t[Property, Range( 0f, 0.4f )]\r\n\tpublic float Irregularity { get; set; } = 0.12f;\r\n\r\n\t[Property, Range( 0f, 0.35f )]\r\n\tpublic float Stretch { get; set; } = 0.08f;\r\n\r\n\t[Property, Range( 0f, 1f )]\r\n\tpublic float Blend { get; set; } = 0.35f;\r\n}\r\n\r\npublic sealed class FlattenBrushSettings\r\n{\r\n\t[Property, Range( 0.05f, 1f )]\r\n\tpublic float Strength { get; set; } = 0.65f;\r\n\r\n\t[Property, Range( 4f, 64f )]\r\n\tpublic float MaxDelta { get; set; } = 24f;\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/Tools/BridgeTool.cs",
            "FileName": "BridgeTool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\n\nusing System;\nusing Sandbox;\nusing Editor.TerrainEditor;\n\nnamespace Editor.SubTerrain;\n\n/// <summary>\n/// Bridge tool. Press on the first point and release on the second to raise a land bridge\n/// spanning the gap. The deck grades from the start ground height to the end ground height,\n/// and the whole span is written once on release rather than painted stroke by stroke.\n/// </summary>\n[Title( \"Bridge\" )]\n[Description( \"Press start, release on end. Raises a land bridge between the two heights.\" )]\n[Icon( \"linear_scale\" )]\n[Alias( \"subterrain_bridge\" )]\n[Group( \"1\" )]\n[Order( 10 )]\npublic class SubBridgeTool : SubTerrainBaseBrushTool\n{\n\tVector3 _startLocal;\n\tVector3 _endLocal;\n\tVector2 _endUV;\n\tfloat _halfWidthTexels;\n\n\tpublic SubBridgeTool( SubTerrainTool parent ) : base( parent )\n\t{\n\t\tMode = SubTerrainSculptMode.Bridge;\n\t}\n\n\tprotected override void OnStrokeStart( Terrain terrain, Vector3 hitPosition )\n\t{\n\t\t_startLocal = hitPosition;\n\t\t_endLocal = hitPosition;\n\n\t\tSlopeStartUV = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize;\n\t\tSlopeStartHeight01 = hitPosition.z / terrain.Storage.TerrainHeight;\n\t}\n\n\tprotected override void OnPaint( Terrain terrain, SubTerrainPaintParameters paint )\n\t{\n\t\t_endLocal = paint.HitPosition;\n\t}\n\n\tprotected override void OnPaintEnded( Terrain terrain )\n\t{\n\t\tBuildBridge( terrain );\n\t\tbase.OnPaintEnded( terrain );\n\t}\n\n\tvoid BuildBridge( Terrain terrain )\n\t{\n\t\tvar storage = terrain.Storage;\n\t\tvar settings = _parent.BrushSettings;\n\n\t\tvar startUV = new Vector2( _startLocal.x, _startLocal.y ) / storage.TerrainSize;\n\t\t_endUV = new Vector2( _endLocal.x, _endLocal.y ) / storage.TerrainSize;\n\n\t\t_halfWidthTexels = Math.Max( settings.BridgeWidth * 0.5f / storage.TerrainSize * storage.Resolution, 1.0f );\n\n\t\tvar spanTexels = (_endUV - startUV) * storage.Resolution;\n\t\tif ( spanTexels.Length < 1.0f )\n\t\t\treturn;\n\n\t\tint size = (int)Math.Ceiling( Math.Max( Math.Abs( spanTexels.x ), Math.Abs( spanTexels.y ) ) + _halfWidthTexels * 2.0f ) + 2;\n\t\tsize = Math.Clamp( size, 1, storage.Resolution );\n\n\t\tSlopeStartUV = startUV;\n\t\tSlopeStartHeight01 = _startLocal.z / storage.TerrainHeight;\n\n\t\tvar paint = new SubTerrainPaintParameters\n\t\t{\n\t\t\tHitPosition = _endLocal,\n\t\t\tHitUV = (startUV + _endUV) * 0.5f,\n\t\t\tFlattenHeight = _endLocal.z / storage.TerrainHeight,\n\t\t\tBrushSettings = settings\n\t\t};\n\n\t\tvar brushTexture = _parent.SelectedBrushTexture;\n\t\tif ( brushTexture is null )\n\t\t\treturn;\n\n\t\tDispatchSculpt( terrain, paint, brushTexture, size, 1.0f );\n\t}\n\n\tprotected override void ApplyExtraAttributes( ComputeShader cs, SubTerrainPaintParameters paint )\n\t{\n\t\tvar settings = paint.BrushSettings;\n\n\t\tcs.Attributes.Set( \"SlopeEndUV\", _endUV );\n\t\tcs.Attributes.Set( \"SlopeEndHeight\", paint.FlattenHeight );\n\t\tcs.Attributes.Set( \"BridgeHalfWidth\", _halfWidthTexels );\n\t\tcs.Attributes.Set( \"BridgeDeckRatio\", 1.0f - settings.BridgeEdgeFalloff );\n\t\tcs.Attributes.Set( \"BridgeRaiseOnly\", settings.BridgeRaiseOnly ? 1 : 0 );\n\t}\n\n\tprotected override void DrawToolOverlay( Terrain terrain, Vector3 worldCenter )\n\t{\n\t\tif ( !_dragging )\n\t\t\treturn;\n\n\t\tvar startWorld = terrain.WorldTransform.PointToWorld( _startLocal );\n\t\tvar span = worldCenter - startWorld;\n\t\tvar flat = new Vector3( span.x, span.y, 0.0f );\n\n\t\tGizmo.Draw.Color = Color.FromBytes( 250, 180, 120 );\n\t\tGizmo.Draw.Line( startWorld, worldCenter );\n\n\t\tif ( flat.Length < 0.001f )\n\t\t\treturn;\n\n\t\tvar halfWidth = _parent.BrushSettings.BridgeWidth * 0.5f;\n\t\tvar side = new Vector3( -flat.y, flat.x, 0.0f ) / flat.Length * halfWidth;\n\n\t\tGizmo.Draw.Line( startWorld + side, worldCenter + side );\n\t\tGizmo.Draw.Line( startWorld - side, worldCenter - side );\n\t\tGizmo.Draw.Line( startWorld - side, startWorld + side );\n\t\tGizmo.Draw.Line( worldCenter - side, worldCenter + side );\n\t}\n}"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/Tools/SetHeightTool.cs",
            "FileName": "SetHeightTool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing Sandbox;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// Flatten to Target Height. Alt-click the ground to load that Z.\r\n/// </summary>\r\n[Title( \"Set Height\" )]\r\n[Icon( \"straighten\" )]\r\n[Alias( \"subterrain_set_height\" )]\r\n[Group( \"1\" )]\r\n[Order( 7 )]\r\npublic class SubSetHeightTool : SubTerrainBaseBrushTool\r\n{\r\n\tpublic SubSetHeightTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.Flatten;\r\n\t\tAllowHeightSample = true;\r\n\t}\r\n\r\n\tprotected override void OnPaint( Terrain terrain, SubTerrainPaintParameters paint )\r\n\t{\r\n\t\tpaint.FlattenHeight = paint.BrushSettings.TargetHeight / terrain.Storage.TerrainHeight;\r\n\t\tbase.OnPaint( terrain, paint );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "TerrainSculpt.cs",
            "FileName": "TerrainSculpt.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\nusing Sandbox.Clutter;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\n/// <summary>\r\n/// Runtime heightfield editor: queue CPU stamps, then SyncGPUTexture + region UpdateCollision.\r\n/// Play-only. Add this next to a Terrain component. One sculpt per Terrain.\r\n/// </summary>\r\n[Title( \"Terrain Sculpt\" )]\r\n[Category( \"SubTerrain\" )]\r\n[Icon( \"landslide\" )]\r\npublic sealed class TerrainSculpt : Component\r\n{\r\n\tconst int MinTexelsAcross = 8;\r\n\r\n\t[Property, Group( \"Limits\" ), Range( 16f, 512f )]\r\n\tpublic float MaxRadius { get; set; } = 240f;\r\n\r\n\t[Property, Group( \"Limits\" ), Range( 0f, 64f )]\r\n\tpublic float FloorMargin { get; set; } = 16f;\r\n\r\n\t[Property, Group( \"Limits\" ), Range( 64f, 2048f )]\r\n\tpublic float GroundDrop { get; set; } = 512f;\r\n\r\n\t[Property, Group( \"Crater\" )]\r\n\tpublic CraterBrushSettings Crater { get; set; } = new();\r\n\r\n\t[Property, Group( \"Dig\" )]\r\n\tpublic DigBrushSettings Dig { get; set; } = new();\r\n\r\n\t[Property, Group( \"Flatten\" )]\r\n\tpublic FlattenBrushSettings Flatten { get; set; } = new();\r\n\r\n\t[Property, Group( \"Paint\" )]\r\n\tpublic bool PaintDirt { get; set; }\r\n\r\n\t[Property, Group( \"Paint\" ), Range( 0, 31 )]\r\n\tpublic int PaintMaterialIndex { get; set; } = 1;\r\n\r\n\t[Property, Group( \"Paint\" ), Range( 0f, 1f )]\r\n\tpublic float PaintStrength { get; set; } = 0.85f;\r\n\r\n\t[Property, Group( \"Perf\" ), Range( 1, 16 )]\r\n\tpublic int StampsPerFrame { get; set; } = 4;\r\n\r\n\t[Property, Group( \"Debug\" )]\r\n\tpublic bool DebugLog { get; set; } = true;\r\n\r\n\tpublic Action<TerrainEditResult> OnApplied { get; set; }\r\n\r\n\t/// <summary>Fired after a successful play-mode reset (Clear Edits / ResetAll). Debris listens here.</summary>\r\n\tpublic Action OnReset { get; set; }\r\n\r\n\treadonly List<TerrainEditRequest> _queue = new();\r\n\treadonly List<TerrainEditResult> _appliedThisFlush = new();\r\n\tushort[] _assetHeight;\r\n\tuint[] _assetControl;\r\n\tushort[] _baseHeight;\r\n\tuint[] _baseControl;\r\n\tfloat _assetZ;\r\n\tbool _floorRaised;\r\n\tbool _warnedLowRes;\r\n\tbool _paintedThisApply;\r\n\tushort[] _mergeScratch;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tif ( !Game.IsPlaying )\r\n\t\t\treturn;\r\n\r\n\t\tvar ok = EnsureSnapshot();\r\n\t\tDbg( ok\r\n\t\t\t? $\"snapshot ready res={GetTerrain()?.Storage?.Resolution} size={GetTerrain()?.Storage?.TerrainSize} height={GetTerrain()?.Storage?.TerrainHeight} texels={_baseHeight?.Length}\"\r\n\t\t\t: \"snapshot FAILED \u2014 no Terrain/Storage on this object\" );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tRestoreFromSnapshot();\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tRestoreFromSnapshot();\r\n\t}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif ( !Game.IsPlaying )\r\n\t\t\treturn;\r\n\r\n\t\tApplyQueued();\r\n\t}\r\n\r\n\t/// <summary>Queue an edit. Returns whether it was accepted (flushes next fixed update).</summary>\r\n\tpublic static bool TryApply( Scene scene, TerrainEditRequest request )\r\n\t{\r\n\t\tif ( scene == null || !scene.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( \"[Sculpt] TryApply \u2014 scene invalid\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\t\tif ( !Game.IsPlaying )\r\n\t\t{\r\n\t\t\tLog.Info( \"[Sculpt] TryApply ignored \u2014 not playing\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\t\tif ( request.Radius <= 0f )\r\n\t\t{\r\n\t\t\tLog.Info( $\"[Sculpt] TryApply ignored \u2014 radius={request.Radius}\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tTerrainSculpt best = null;\r\n\t\tvar bestDist = float.MaxValue;\r\n\t\tvar candidates = 0;\r\n\r\n\t\tforeach ( var sculpt in scene.GetAllComponents<TerrainSculpt>() )\r\n\t\t{\r\n\t\t\tcandidates++;\r\n\t\t\tif ( !sculpt.Active )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar terrain = sculpt.GetTerrain();\r\n\t\t\tif ( terrain == null || !terrain.IsValid() || terrain.Storage == null )\r\n\t\t\t{\r\n\t\t\t\tsculpt.Dbg( \"TryApply skip \u2014 no Terrain/Storage\" );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !sculpt.TryProjectToTerrain( terrain, request.WorldPos, out var local ) )\r\n\t\t\t{\r\n\t\t\t\tsculpt.Dbg( $\"TryApply miss project pos={request.WorldPos} drop={sculpt.GroundDrop}\" );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar hitWorld = terrain.WorldTransform.PointToWorld( local );\r\n\t\t\tvar dist = request.WorldPos.DistanceSquared( hitWorld );\r\n\t\t\tif ( dist >= bestDist )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tbestDist = dist;\r\n\t\t\tbest = sculpt;\r\n\t\t}\r\n\r\n\t\tif ( best == null )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"[Sculpt] TryApply \u2014 no terrain under {request.WorldPos} (components={candidates})\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif ( request.Seed == 0 )\r\n\t\t\trequest.Seed = Game.Random.Int( 1, 1_000_000_000 );\r\n\r\n\t\tbest.Dbg( $\"TryApply hit kind={request.Kind} dist={MathF.Sqrt( bestDist ):0.#} pos={request.WorldPos}\" );\r\n\t\tbest._queue.Add( request );\r\n\t\treturn true;\r\n\t}\r\n\r\n\tpublic static int ResetAll( Scene scene )\r\n\t{\r\n\t\tif ( scene == null || !scene.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( \"[Sculpt] ResetAll \u2014 scene invalid\" );\r\n\t\t\treturn 0;\r\n\t\t}\r\n\r\n\t\tvar cleared = 0;\r\n\t\tforeach ( var sculpt in scene.GetAllComponents<TerrainSculpt>() )\r\n\t\t{\r\n\t\t\tif ( sculpt.ResetEdits() )\r\n\t\t\t\tcleared++;\r\n\t\t}\r\n\r\n\t\tLog.Info( $\"[Sculpt] ResetAll \u2014 cleared {cleared} terrain(s)\" );\r\n\t\treturn cleared;\r\n\t}\r\n\r\n\t[Button( \"Clear Edits\" )]\r\n\tpublic void ClearEdits()\r\n\t{\r\n\t\tResetEdits();\r\n\t}\r\n\r\n\t[Button( \"Clear All Scene Edits\" )]\r\n\tpublic void ClearAllSceneEdits()\r\n\t{\r\n\t\tResetAll( Scene );\r\n\t}\r\n\r\n\tpublic bool ResetEdits()\r\n\t{\r\n\t\tif ( _baseHeight == null )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"[Sculpt] {GameObject.Name}: Clear Edits \u2014 no snapshot (play first, or already restored)\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tvar terrain = GetTerrain();\r\n\t\tif ( terrain == null || !terrain.IsValid() || terrain.Storage == null )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"[Sculpt] {GameObject.Name}: Clear Edits \u2014 no Terrain/Storage\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tvar storage = terrain.Storage;\r\n\t\tif ( storage.HeightMap.Length != _baseHeight.Length )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"[Sculpt] {GameObject.Name}: Clear Edits \u2014 size mismatch snapshot={_baseHeight.Length} map={storage.HeightMap.Length}\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\t_baseHeight.CopyTo( storage.HeightMap, 0 );\r\n\t\tif ( _baseControl != null && storage.ControlMap.Length == _baseControl.Length )\r\n\t\t\t_baseControl.CopyTo( storage.ControlMap, 0 );\r\n\r\n\t\tPushToEngine( terrain, FullRect( storage.Resolution ), painted: _baseControl != null );\r\n\t\t_queue.Clear();\r\n\t\tOnReset?.Invoke();\r\n\t\tLog.Info( $\"[Sculpt] {GameObject.Name}: edits cleared\" );\r\n\t\treturn true;\r\n\t}\r\n\r\n\tvoid ApplyQueued()\r\n\t{\r\n\t\tif ( _queue.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar terrain = GetTerrain();\r\n\t\tif ( terrain == null || !terrain.IsValid() || terrain.Storage == null )\r\n\t\t{\r\n\t\t\tDbg( $\"ApplyQueued \u2014 dropped {_queue.Count} stamps (no Terrain/Storage)\" );\r\n\t\t\t_queue.Clear();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !EnsureSnapshot() )\r\n\t\t{\r\n\t\t\tDbg( \"ApplyQueued \u2014 snapshot failed\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar limit = Math.Clamp( StampsPerFrame, 1, 16 );\r\n\t\tvar take = Math.Min( limit, _queue.Count );\r\n\r\n\t\tRectInt dirty = default;\r\n\t\tvar any = false;\r\n\t\t_paintedThisApply = false;\r\n\t\t_appliedThisFlush.Clear();\r\n\r\n\t\tfor ( var i = 0; i < take; i++ )\r\n\t\t{\r\n\t\t\tif ( !ApplyStamp( terrain, _queue[i], out var rect, out var result ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t_appliedThisFlush.Add( result );\r\n\t\t\tif ( !any )\r\n\t\t\t{\r\n\t\t\t\tdirty = rect;\r\n\t\t\t\tany = true;\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tdirty.Add( rect );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_queue.RemoveRange( 0, take );\r\n\r\n\t\tif ( !any )\r\n\t\t{\r\n\t\t\tDbg( \"ApplyQueued \u2014 all stamps missed terrain\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tPushToEngine( terrain, dirty, _paintedThisApply );\r\n\t\tDbg( $\"pushed dirty=({dirty.Left},{dirty.Top},{dirty.Width}x{dirty.Height}) painted={_paintedThisApply} remaining={_queue.Count}\" );\r\n\r\n\t\tvar applied = OnApplied;\r\n\t\tif ( applied == null )\r\n\t\t\treturn;\r\n\r\n\t\tfor ( var i = 0; i < _appliedThisFlush.Count; i++ )\r\n\t\t\tapplied( _appliedThisFlush[i] );\r\n\t}\r\n\r\n\tbool ApplyStamp( Terrain terrain, TerrainEditRequest request, out RectInt dirty, out TerrainEditResult result )\r\n\t{\r\n\t\tdirty = default;\r\n\t\tresult = default;\r\n\r\n\t\tvar storage = terrain.Storage;\r\n\t\tif ( storage == null || _baseHeight == null )\r\n\t\t\treturn false;\r\n\r\n\t\tvar radius = Math.Clamp( request.Radius, 1f, MaxRadius );\r\n\t\tvar depth = ResolveDepth( request.Kind, radius, request.Depth );\r\n\t\tif ( request.Kind != TerrainEditKind.Flatten && depth <= 0f )\r\n\t\t{\r\n\t\t\tDbg( $\"ApplyStamp \u2014 depth 0 kind={request.Kind}\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif ( !TryProjectToTerrain( terrain, request.WorldPos, out var local ) )\r\n\t\t{\r\n\t\t\tDbg( $\"ApplyStamp \u2014 project miss world={request.WorldPos}\" );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tvar size = storage.TerrainSize;\r\n\t\tvar height = storage.TerrainHeight;\r\n\t\tvar res = storage.Resolution;\r\n\t\tif ( size <= 0f || height <= 0f || res <= 0 )\r\n\t\t\treturn false;\r\n\r\n\t\tvar uv = new Vector2( local.x, local.y ) / size;\r\n\t\tif ( uv.x < 0f || uv.y < 0f || uv.x > 1f || uv.y > 1f )\r\n\t\t\treturn false;\r\n\r\n\t\tvar texelSize = size / res;\r\n\t\tvar across = radius * 2f / texelSize;\r\n\t\tif ( across < MinTexelsAcross && !_warnedLowRes )\r\n\t\t{\r\n\t\t\t_warnedLowRes = true;\r\n\t\t\tLog.Warning( $\"[TerrainSculpt] Radius {radius:0} spans {across:0.0} texels (need >={MinTexelsAcross} to hide a character). texelSize={texelSize:0.0}.\" );\r\n\t\t}\r\n\r\n\t\tvar cx = (int)Math.Floor( res * uv.x );\r\n\t\tvar cy = (int)Math.Floor( res * uv.y );\r\n\t\tvar irreg = request.Kind switch\r\n\t\t{\r\n\t\t\tTerrainEditKind.Dig => Math.Clamp( Dig.Irregularity, 0f, 0.4f ),\r\n\t\t\tTerrainEditKind.Flatten => 0f,\r\n\t\t\t_ => Math.Clamp( Crater.Irregularity, 0f, 0.7f )\r\n\t\t};\r\n\t\tvar stretchAmt = request.Kind switch\r\n\t\t{\r\n\t\t\tTerrainEditKind.Dig => Math.Clamp( Dig.Stretch, 0f, 0.35f ),\r\n\t\t\tTerrainEditKind.Flatten => 0f,\r\n\t\t\t_ => Math.Clamp( Crater.Stretch, 0f, 0.5f )\r\n\t\t};\r\n\t\tvar blend = request.Kind switch\r\n\t\t{\r\n\t\t\tTerrainEditKind.Dig => Math.Clamp( Dig.Blend, 0f, 1f ),\r\n\t\t\tTerrainEditKind.Flatten => 0f,\r\n\t\t\t_ => Math.Clamp( Crater.Blend, 0f, 1f )\r\n\t\t};\r\n\t\tvar shapePad = 1f + irreg + stretchAmt + 0.25f + blend * 0.15f;\r\n\t\tvar radTexels = Math.Max( 1, (int)Math.Ceiling( radius / texelSize * shapePad ) + 3 );\r\n\t\tdirty = new RectInt( cx - radTexels, cy - radTexels, radTexels * 2 + 1, radTexels * 2 + 1 );\r\n\r\n\t\tvar x0 = Math.Clamp( dirty.Left, 0, res - 1 );\r\n\t\tvar y0 = Math.Clamp( dirty.Top, 0, res - 1 );\r\n\t\tvar x1 = Math.Clamp( dirty.Right, 0, res );\r\n\t\tvar y1 = Math.Clamp( dirty.Bottom, 0, res );\r\n\t\tif ( x1 <= x0 || y1 <= y0 )\r\n\t\t\treturn false;\r\n\r\n\t\tdirty = new RectInt( x0, y0, x1 - x0, y1 - y0 );\r\n\r\n\t\tvar crater = Crater;\r\n\t\tvar dig = Dig;\r\n\t\tvar maxDepth = request.Kind == TerrainEditKind.Dig ? dig.MaxDepth : crater.MaxDepth;\r\n\t\tvar maxRaise = request.Kind == TerrainEditKind.Dig ? dig.MaxRaise : crater.MaxRaise;\r\n\t\tvar bowlFrac = request.Kind == TerrainEditKind.Dig\r\n\t\t\t? Math.Clamp( dig.BowlRadius, 0.7f, 0.98f )\r\n\t\t\t: Math.Clamp( crater.BowlRadius, 0.4f, 0.95f );\r\n\r\n\t\tvar depthU = WorldToUshort( depth, height );\r\n\t\tvar maxDepthU = WorldToUshort( maxDepth, height );\r\n\t\tvar raiseU = WorldToUshort( maxRaise, height );\r\n\r\n\t\tvar heights = storage.HeightMap;\r\n\t\tvar control = storage.ControlMap;\r\n\t\tvar paint = PaintDirt && PaintMaterialIndex >= 0\r\n\t\t\t&& storage.Materials != null\r\n\t\t\t&& PaintMaterialIndex < storage.Materials.Count;\r\n\r\n\t\tif ( paint )\r\n\t\t\tEnsureControlSnapshot();\r\n\r\n\t\tvar originX = uv.x * res;\r\n\t\tvar originY = uv.y * res;\r\n\t\tvar radiusTexels = radius / texelSize;\r\n\t\tif ( radiusTexels < 0.5f )\r\n\t\t\treturn false;\r\n\r\n\t\tvar rng = new Random( request.Seed == 0 ? 1 : request.Seed );\r\n\t\tvar rot = rng.Float( 0f, MathF.PI * 2f );\r\n\t\tvar stretchX = Math.Clamp( 1f + rng.Float( -stretchAmt, stretchAmt ), 0.55f, 1.7f );\r\n\t\tif ( stretchAmt <= 0.001f )\r\n\t\t\tstretchX = 1f;\r\n\r\n\t\tvar sampleX = Math.Clamp( cx, x0, x1 - 1 );\r\n\t\tvar sampleY = Math.Clamp( cy, y0, y1 - 1 );\r\n\t\tvar sampleI = sampleX + sampleY * res;\r\n\t\tvar heightBefore = heights[sampleI];\r\n\r\n\t\tvar flattenTargetU = (int)heightBefore;\r\n\t\tif ( request.Kind == TerrainEditKind.Flatten && request.HasTargetZ )\r\n\t\t{\r\n\t\t\tvar targetLocal = terrain.WorldTransform.PointToLocal( new Vector3( request.WorldPos.x, request.WorldPos.y, request.TargetZ ) );\r\n\t\t\tflattenTargetU = WorldToUshort( targetLocal.z, height );\r\n\t\t}\r\n\r\n\t\tvar ctx = new TerrainBrushes.Context\r\n\t\t{\r\n\t\t\tHeights = heights,\r\n\t\t\tBase = _baseHeight,\r\n\t\t\tControl = control,\r\n\t\t\tRes = res,\r\n\t\t\tX0 = x0,\r\n\t\t\tY0 = y0,\r\n\t\t\tX1 = x1,\r\n\t\t\tY1 = y1,\r\n\t\t\tOriginX = originX,\r\n\t\t\tOriginY = originY,\r\n\t\t\tRadiusTexels = radiusTexels,\r\n\t\t\tCa = MathF.Cos( rot ),\r\n\t\t\tSa = MathF.Sin( rot ),\r\n\t\t\tStretchX = stretchX,\r\n\t\t\tStretchY = 1f / stretchX,\r\n\t\t\tOx = rng.Float( 0f, 64f ),\r\n\t\t\tOy = rng.Float( 0f, 64f ),\r\n\t\t\tOz = rng.Float( 0f, 64f ),\r\n\t\t\tFreq = 2.1f + rng.Float( 0f, 2.4f ),\r\n\t\t\tIrreg = irreg,\r\n\t\t\tDepthU = depthU,\r\n\t\t\tMaxDepthU = maxDepthU,\r\n\t\t\tRaiseU = raiseU,\r\n\t\t\tBowlFrac = bowlFrac,\r\n\t\t\tSmoothK = blend * depthU * 0.55f,\r\n\t\t\tDugU = Math.Max( 2, WorldToUshort( 8f, height ) ),\r\n\t\t\tPaint = paint,\r\n\t\t\tPaintIndex = PaintMaterialIndex,\r\n\t\t\tPaintStrength = PaintStrength,\r\n\t\t\tFlattenTargetU = flattenTargetU,\r\n\t\t\tFlattenStrength = Math.Clamp( Flatten.Strength, 0.05f, 1f ),\r\n\t\t\tFlattenMaxDeltaU = WorldToUshort( Flatten.MaxDelta, height ),\r\n\t\t\tSpoilAngle = rng.Float( 0f, MathF.PI * 2f )\r\n\t\t};\r\n\r\n\t\tif ( request.Kind != TerrainEditKind.Flatten )\r\n\t\t{\r\n\t\t\tvar eccA = rng.Float( 0f, MathF.PI * 2f );\r\n\t\t\tvar ecc = rng.Float( 0f, irreg * 0.22f ) * radiusTexels;\r\n\t\t\tctx.OriginX = originX + MathF.Cos( eccA ) * ecc;\r\n\t\t\tctx.OriginY = originY + MathF.Sin( eccA ) * ecc;\r\n\t\t}\r\n\r\n\t\tswitch ( request.Kind )\r\n\t\t{\r\n\t\t\tcase TerrainEditKind.Dig:\r\n\t\t\t\tTerrainBrushes.ApplyDig( ref ctx );\r\n\t\t\t\tbreak;\r\n\t\t\tcase TerrainEditKind.Flatten:\r\n\t\t\t\tTerrainBrushes.ApplyFlatten( ref ctx );\r\n\t\t\t\tbreak;\r\n\t\t\tdefault:\r\n\t\t\t\tTerrainBrushes.ApplyCrater( ref ctx );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\r\n\t\tif ( ctx.Painted )\r\n\t\t\t_paintedThisApply = true;\r\n\r\n\t\tif ( blend > 0.01f && request.Kind != TerrainEditKind.Flatten )\r\n\t\t\tBreakSaddles( storage, x0, y0, x1, y1, maxDepthU, ctx.DugU, blend, paint );\r\n\r\n\t\tvar heightAfter = heights[sampleI];\r\n\t\tvar dropWorld = (heightBefore - heightAfter) / (float)ushort.MaxValue * height;\r\n\t\tvar volume = ctx.VolumeU / (float)ushort.MaxValue * height * texelSize * texelSize;\r\n\t\tDbg( $\"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})\" );\r\n\r\n\t\tresult = new TerrainEditResult\r\n\t\t{\r\n\t\t\tKind = request.Kind,\r\n\t\t\tOrigin = request.WorldPos,\r\n\t\t\tRadius = radius,\r\n\t\t\tDepth = depth,\r\n\t\t\tDirty = dirty,\r\n\t\t\tTexels = ctx.Written,\r\n\t\t\tVolumeRemoved = volume\r\n\t\t};\r\n\t\treturn true;\r\n\t}\r\n\r\n\tvoid BreakSaddles( TerrainStorage storage, int x0, int y0, int x1, int y1, int maxDepthU, int dugU, float blend, bool paint )\r\n\t{\r\n\t\tif ( _baseHeight == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar res = storage.Resolution;\r\n\t\tvar w = x1 - x0;\r\n\t\tvar hgt = y1 - y0;\r\n\t\tif ( w < 3 || hgt < 3 )\r\n\t\t\treturn;\r\n\r\n\t\tvar need = w * hgt;\r\n\t\tif ( _mergeScratch == null || _mergeScratch.Length < need )\r\n\t\t\t_mergeScratch = new ushort[need];\r\n\r\n\t\tvar heights = storage.HeightMap;\r\n\t\tvar control = storage.ControlMap;\r\n\t\tvar scratch = _mergeScratch;\r\n\r\n\t\tfor ( var y = 0; y < hgt; y++ )\r\n\t\t{\r\n\t\t\tvar src = (y0 + y) * res + x0;\r\n\t\t\tArray.Copy( heights, src, scratch, y * w, w );\r\n\t\t}\r\n\r\n\t\tfor ( var y = 1; y < hgt - 1; y++ )\r\n\t\t{\r\n\t\t\tfor ( var x = 1; x < w - 1; x++ )\r\n\t\t\t{\r\n\t\t\t\tvar i = (y0 + y) * res + (x0 + x);\r\n\t\t\t\tvar row = y * w + x;\r\n\t\t\t\tvar left = scratch[row - 1];\r\n\t\t\t\tvar right = scratch[row + 1];\r\n\t\t\t\tvar down = scratch[row - w];\r\n\t\t\t\tvar up = scratch[row + w];\r\n\t\t\t\tvar leftD = (int)_baseHeight[i - 1] - left >= dugU;\r\n\t\t\t\tvar rightD = (int)_baseHeight[i + 1] - right >= dugU;\r\n\t\t\t\tvar downD = (int)_baseHeight[i - res] - down >= dugU;\r\n\t\t\t\tvar upD = (int)_baseHeight[i + res] - up >= dugU;\r\n\t\t\t\tif ( !((leftD && rightD) || (downD && upD)) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar minN = int.MaxValue;\r\n\t\t\t\tif ( leftD ) minN = Math.Min( minN, left );\r\n\t\t\t\tif ( rightD ) minN = Math.Min( minN, right );\r\n\t\t\t\tif ( downD ) minN = Math.Min( minN, down );\r\n\t\t\t\tif ( upD ) minN = Math.Min( minN, up );\r\n\r\n\t\t\t\tvar self = scratch[row];\r\n\t\t\t\tif ( minN >= self )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar b = (int)_baseHeight[i];\r\n\t\t\t\tvar merged = (int)MathF.Round( self + (minN - self) * blend );\r\n\t\t\t\tmerged = Math.Max( merged, b - maxDepthU );\r\n\t\t\t\theights[i] = (ushort)Math.Clamp( merged, 0, ushort.MaxValue );\r\n\r\n\t\t\t\tif ( !paint || control == null )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar mat = new CompactTerrainMaterial( control[i] );\r\n\t\t\t\tif ( mat.IsHole )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tmat.OverlayTextureId = (byte)PaintMaterialIndex;\r\n\t\t\t\tmat.BlendFactor = (byte)Math.Clamp( mat.BlendFactor + (int)(blend * 80f), 0, 255 );\r\n\t\t\t\tcontrol[i] = mat.Packed;\r\n\t\t\t\t_paintedThisApply = true;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tbool TryProjectToTerrain( Terrain terrain, Vector3 worldPos, out Vector3 local )\r\n\t{\r\n\t\tlocal = default;\r\n\t\tvar origin = worldPos + Vector3.Up * 8f;\r\n\t\tvar ray = new Ray( origin, Vector3.Down );\r\n\t\tif ( terrain.RayIntersects( ray, GroundDrop + 8f, out local ) )\r\n\t\t\treturn true;\r\n\r\n\t\tlocal = terrain.WorldTransform.PointToLocal( worldPos );\r\n\t\tvar size = terrain.Storage?.TerrainSize ?? 0f;\r\n\t\treturn local.x >= 0f && local.y >= 0f && local.x <= size && local.y <= size;\r\n\t}\r\n\r\n\tvoid PushToEngine( Terrain terrain, RectInt region, bool painted )\r\n\t{\r\n\t\tif ( region.Width < 1 || region.Height < 1 )\r\n\t\t\treturn;\r\n\r\n\t\tif ( terrain.HeightMap != null )\r\n\t\t\tterrain.SyncGPUTexture();\r\n\r\n\t\tvar flags = Terrain.SyncFlags.Height;\r\n\t\tif ( painted )\r\n\t\t\tflags |= Terrain.SyncFlags.Control;\r\n\r\n\t\tterrain.UpdateCollision( flags, region );\r\n\t\tInvalidateClutter( terrain, region );\r\n\t}\r\n\r\n\tvoid InvalidateClutter( Terrain terrain, RectInt region )\r\n\t{\r\n\t\tif ( !Scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar clutter = Scene.GetSystem<ClutterGridSystem>();\r\n\t\tif ( clutter == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar storage = terrain.Storage;\r\n\t\tif ( storage == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar res = storage.Resolution;\r\n\t\tvar size = storage.TerrainSize;\r\n\t\tvar minLocal = new Vector3( region.Left / (float)res * size, region.Top / (float)res * size, -1000f );\r\n\t\tvar maxLocal = new Vector3( region.Right / (float)res * size, region.Bottom / (float)res * size, 1000f );\r\n\t\tvar a = terrain.WorldTransform.PointToWorld( minLocal );\r\n\t\tvar b = terrain.WorldTransform.PointToWorld( maxLocal );\r\n\t\tclutter.InvalidateTilesInBounds( new BBox( a, b ) );\r\n\t}\r\n\r\n\tbool EnsureSnapshot()\r\n\t{\r\n\t\tvar terrain = GetTerrain();\r\n\t\tif ( terrain == null || !terrain.IsValid() || terrain.Storage?.HeightMap == null )\r\n\t\t\treturn false;\r\n\r\n\t\tvar map = terrain.Storage.HeightMap;\r\n\t\tif ( _baseHeight != null && _baseHeight.Length == map.Length )\r\n\t\t\treturn true;\r\n\r\n\t\t_assetZ = WorldPosition.z;\r\n\t\t_assetHeight = new ushort[map.Length];\r\n\t\tmap.CopyTo( _assetHeight, 0 );\r\n\t\t_baseHeight = new ushort[map.Length];\r\n\t\tmap.CopyTo( _baseHeight, 0 );\r\n\t\tSnapshotControl();\r\n\t\tRaiseFloor( terrain );\r\n\t\treturn true;\r\n\t}\r\n\r\n\tvoid SnapshotControl()\r\n\t{\r\n\t\tvar terrain = GetTerrain();\r\n\t\tif ( terrain?.Storage?.ControlMap == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar control = terrain.Storage.ControlMap;\r\n\t\t_assetControl = new uint[control.Length];\r\n\t\tcontrol.CopyTo( _assetControl, 0 );\r\n\t\t_baseControl = new uint[control.Length];\r\n\t\tcontrol.CopyTo( _baseControl, 0 );\r\n\t}\r\n\r\n\tvoid EnsureControlSnapshot()\r\n\t{\r\n\t\tif ( _baseControl != null )\r\n\t\t\treturn;\r\n\r\n\t\tSnapshotControl();\r\n\t}\r\n\r\n\tvoid RaiseFloor( Terrain terrain )\r\n\t{\r\n\t\tvar storage = terrain.Storage;\r\n\t\tif ( storage == null || _baseHeight == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar height = storage.TerrainHeight;\r\n\t\tvar needWorld = MathF.Max( Crater.MaxDepth, Dig.MaxDepth ) + FloorMargin;\r\n\t\tvar needU = WorldToUshort( needWorld, height );\r\n\r\n\t\tvar min = ushort.MaxValue;\r\n\t\tfor ( var i = 0; i < _baseHeight.Length; i++ )\r\n\t\t{\r\n\t\t\tif ( _baseHeight[i] < min )\r\n\t\t\t\tmin = _baseHeight[i];\r\n\t\t}\r\n\r\n\t\tif ( min >= needU )\r\n\t\t{\r\n\t\t\tDbg( $\"floor ok min={min} need={needU} ({needWorld:0.#}u of {height:0.#})\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar pad = needU - min;\r\n\t\tfor ( var i = 0; i < _baseHeight.Length; i++ )\r\n\t\t{\r\n\t\t\tvar v = (int)_baseHeight[i] + pad;\r\n\t\t\t_baseHeight[i] = (ushort)Math.Min( v, ushort.MaxValue );\r\n\t\t}\r\n\r\n\t\t_baseHeight.CopyTo( storage.HeightMap, 0 );\r\n\r\n\t\tvar padWorld = pad / (float)ushort.MaxValue * height;\r\n\t\tWorldPosition = WorldPosition.WithZ( _assetZ - padWorld );\r\n\t\t_floorRaised = true;\r\n\r\n\t\tPushToEngine( terrain, FullRect( storage.Resolution ), painted: false );\r\n\t\tDbg( $\"raised floor pad={pad} ({padWorld:0.#}u) min {min}->{min + pad} need={needU} z {_assetZ:0.#}->{WorldPosition.z:0.#}\" );\r\n\t}\r\n\r\n\tvoid RestoreFromSnapshot()\r\n\t{\r\n\t\tif ( _assetHeight == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar terrain = GetTerrain();\r\n\t\tif ( terrain == null || !terrain.IsValid() || terrain.Storage == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar storage = terrain.Storage;\r\n\t\tif ( storage.HeightMap.Length != _assetHeight.Length )\r\n\t\t\treturn;\r\n\r\n\t\t_assetHeight.CopyTo( storage.HeightMap, 0 );\r\n\t\tif ( _assetControl != null && storage.ControlMap.Length == _assetControl.Length )\r\n\t\t\t_assetControl.CopyTo( storage.ControlMap, 0 );\r\n\r\n\t\tif ( _floorRaised )\r\n\t\t{\r\n\t\t\tWorldPosition = WorldPosition.WithZ( _assetZ );\r\n\t\t\t_floorRaised = false;\r\n\t\t}\r\n\r\n\t\tPushToEngine( terrain, FullRect( storage.Resolution ), painted: _assetControl != null );\r\n\t\t_queue.Clear();\r\n\t\t_assetHeight = null;\r\n\t\t_assetControl = null;\r\n\t\t_baseHeight = null;\r\n\t\t_baseControl = null;\r\n\t\tDbg( \"RestoreFromSnapshot \u2014 disk heightmap restored\" );\r\n\t}\r\n\r\n\tfloat ResolveDepth( TerrainEditKind kind, float radius, float depth )\r\n\t{\r\n\t\tif ( kind == TerrainEditKind.Flatten )\r\n\t\t\treturn 0f;\r\n\r\n\t\tvar scale = kind == TerrainEditKind.Dig ? Dig.DepthScale : Crater.DepthScale;\r\n\t\tvar max = kind == TerrainEditKind.Dig ? Dig.MaxDepth : Crater.MaxDepth;\r\n\t\tvar d = depth > 0f ? depth : radius * scale;\r\n\t\treturn Math.Clamp( d, 0f, max );\r\n\t}\r\n\r\n\tTerrain GetTerrain()\r\n\t{\r\n\t\treturn Components.Get<Terrain>( FindMode.EverythingInSelfAndDescendants );\r\n\t}\r\n\r\n\tstatic RectInt FullRect( int resolution ) => new( 0, 0, resolution, resolution );\r\n\r\n\tstatic int WorldToUshort( float world, float terrainHeight )\r\n\t{\r\n\t\tif ( terrainHeight <= 0f )\r\n\t\t\treturn 0;\r\n\t\treturn (int)Math.Clamp( MathF.Round( world / terrainHeight * ushort.MaxValue ), 0f, (float)ushort.MaxValue );\r\n\t}\r\n\r\n\tvoid Dbg( string message )\r\n\t{\r\n\t\tif ( !DebugLog )\r\n\t\t\treturn;\r\n\r\n\t\tLog.Info( $\"[Sculpt] {GameObject.Name}: {message}\" );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Code/TerrainBrushSettings.cs",
            "FileName": "TerrainBrushSettings.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\npublic sealed class CraterBrushSettings\r\n{\r\n\t[Property, Range( 16f, 200f )]\r\n\tpublic float MaxDepth { get; set; } = 96f;\r\n\r\n\t[Property, Title( \"Depth Scale\" ), Range( 0.1f, 1.5f )]\r\n\tpublic float DepthScale { get; set; } = 0.5f;\r\n\r\n\t[Property, Range( 0f, 32f )]\r\n\tpublic float MaxRaise { get; set; } = 12f;\r\n\r\n\t[Property, Range( 0.4f, 0.95f )]\r\n\tpublic float BowlRadius { get; set; } = 0.75f;\r\n\r\n\t[Property, Range( 0f, 0.7f )]\r\n\tpublic float Irregularity { get; set; } = 0.35f;\r\n\r\n\t[Property, Range( 0f, 0.5f )]\r\n\tpublic float Stretch { get; set; } = 0.25f;\r\n\r\n\t[Property, Range( 0f, 1f )]\r\n\tpublic float Blend { get; set; } = 0.65f;\r\n}\r\n\r\npublic sealed class DigBrushSettings\r\n{\r\n\t[Property, Range( 16f, 120f )]\r\n\tpublic float MaxDepth { get; set; } = 48f;\r\n\r\n\t[Property, Title( \"Depth Scale\" ), Range( 0.1f, 1.5f )]\r\n\tpublic float DepthScale { get; set; } = 0.85f;\r\n\r\n\t[Property, Range( 0f, 40f )]\r\n\tpublic float MaxRaise { get; set; } = 18f;\r\n\r\n\t[Property, Range( 0.7f, 0.98f )]\r\n\tpublic float BowlRadius { get; set; } = 0.9f;\r\n\r\n\t[Property, Range( 0f, 0.4f )]\r\n\tpublic float Irregularity { get; set; } = 0.12f;\r\n\r\n\t[Property, Range( 0f, 0.35f )]\r\n\tpublic float Stretch { get; set; } = 0.08f;\r\n\r\n\t[Property, Range( 0f, 1f )]\r\n\tpublic float Blend { get; set; } = 0.35f;\r\n}\r\n\r\npublic sealed class FlattenBrushSettings\r\n{\r\n\t[Property, Range( 0.05f, 1f )]\r\n\tpublic float Strength { get; set; } = 0.65f;\r\n\r\n\t[Property, Range( 4f, 64f )]\r\n\tpublic float MaxDelta { get; set; } = 24f;\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/SubTerrainPaintSettings.cs",
            "FileName": "SubTerrainPaintSettings.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// Paint Texture settings. Ground is the default mode. UI labels live on the [Title]\r\n/// attributes; OnlyOnSlope has no inspector title (Steep | All owns it).\r\n/// </summary>\r\npublic class SubTerrainPaintSettings\r\n{\r\n\tpublic const float WearMixMax = 0.40f;\r\n\tpublic const float PathMixMax = 0.36f;\r\n\tpublic const float DitherWarn = 0.36f;\r\n\tpublic const float PackCoreMin = 0.15f;\r\n\tpublic const float PackCoreMax = 0.95f;\r\n\tpublic const string LibraryVersion = \"1.4.15\";\r\n\r\n\tpublic SubTerrainPaintLayer Layer { get; set; } = SubTerrainPaintLayer.Base;\r\n\r\n\tpublic int MaterialIndex { get; set; }\r\n\r\n\tpublic float MixCap { get; set; } = 0.28f;\r\n\r\n\t/// <summary>Wear only. Move mix on the pair that is already there. The selected tmat is not written.</summary>\r\n\t[Property, Title( \"Keep pair\" ), WideMode]\r\n\tpublic bool MixOnly { get; set; }\r\n\r\n\t/// <summary>Alt-click the ground to stay on. Wear will not paint other bases.</summary>\r\n\t[Property, Title( \"Stay on sampled\" ), WideMode]\r\n\tpublic bool LimitToSampledGround { get; set; }\r\n\r\n\t/// <summary>Path only. Hard brush center packs the path tmat as base.</summary>\r\n\t[Property, Title( \"Pack center\" ), Description( \"On: solid path core along the stroke, soft apron outside. Off: soft ground + path apron only.\" ), WideMode]\r\n\tpublic bool PackCenter { get; set; } = true;\r\n\r\n\t/// <summary>Path + Pack center. Higher = wider solid middle. Outer ring stays soft blend.</summary>\r\n\t[Property, Title( \"Core\" ), Description( \"How much of the brush packs solid. Raise for a wider solid middle. Outer ring stays Center mix apron.\" ), Range( PackCoreMin, PackCoreMax ), Step( 0.01f ), WideMode]\r\n\tpublic float PackCore { get; set; } = 0.55f;\r\n\r\n\t[Property, Title( \"How worn\" ), Description( \"Overlay mix. Default 0.28. Soft chew to 0.36; hard stop 0.40. Above 0.36 can dither.\" ), Range( 0.05f, WearMixMax ), Step( 0.01f ), WideMode]\r\n\tpublic float HowWorn\r\n\t{\r\n\t\tget => MixCap;\r\n\t\tset => MixCap = value;\r\n\t}\r\n\r\n\t[Property, Title( \"Center mix\" ), Description( \"Soft apron overlay mix outside the packed core. Raise with brush Size for a wider blend.\" ), Range( 0.05f, PathMixMax ), Step( 0.01f ), WideMode]\r\n\tpublic float CenterMix\r\n\t{\r\n\t\tget => MixCap;\r\n\t\tset => MixCap = value;\r\n\t}\r\n\r\n\tpublic bool OnlyOnSlope { get; set; } = true;\r\n\r\n\t[Property, Title( \"Steeper than\" ), Description( \"How steep the ground must be. 20 is a hill face. Lower it to catch gentler banks.\" ), Range( 1f, 60f ), Step( 1f ), WideMode]\r\n\tpublic float SlopeDegrees { get; set; } = 20f;\r\n\r\n\tpublic int SampledGroundIndex { get; set; } = -1;\r\n\r\n\tpublic int PathGroundIndex { get; set; } = -1;\r\n\r\n\tpublic bool HasSampledGround => SampledGroundIndex >= 0;\r\n\r\n\tpublic bool HasPathGround => PathGroundIndex >= 0;\r\n\r\n\tpublic bool Dithers => MixCap > DitherWarn;\r\n\r\n\t/// <summary>Stamp value above which Pack center writes solid path. Higher Core = lower threshold.</summary>\r\n\tpublic float PackThreshold\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tfloat core = Math.Clamp( PackCore, PackCoreMin, PackCoreMax );\r\n\t\t\treturn Math.Clamp( 0.82f - core * 0.64f, 0.15f, 0.85f );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static float MixMaxFor( SubTerrainPaintLayer layer )\r\n\t{\r\n\t\treturn layer == SubTerrainPaintLayer.Path ? PathMixMax : WearMixMax;\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Per Ground / Wear / Path mix and gate, for the editor session.\r\n/// </summary>\r\ninternal static class SubTerrainPaintStore\r\n{\r\n\tstruct LayerState\r\n\t{\r\n\t\tpublic float MixCap;\r\n\t\tpublic bool OnlyOnSlope;\r\n\t\tpublic float SlopeDegrees;\r\n\t\tpublic bool MixOnly;\r\n\t\tpublic bool LimitToSampledGround;\r\n\t\tpublic bool PackCenter;\r\n\t\tpublic float PackCore;\r\n\t}\r\n\r\n\tstatic readonly Dictionary<SubTerrainPaintLayer, LayerState> ByLayer = new();\r\n\r\n\tpublic static void Capture( SubTerrainPaintSettings settings )\r\n\t{\r\n\t\tif ( settings is null )\r\n\t\t\treturn;\r\n\r\n\t\tByLayer[settings.Layer] = Read( settings );\r\n\t}\r\n\r\n\tpublic static void Apply( SubTerrainPaintSettings settings, SubTerrainPaintLayer layer )\r\n\t{\r\n\t\tif ( settings is null )\r\n\t\t\treturn;\r\n\r\n\t\tif ( !ByLayer.TryGetValue( layer, out var state ) )\r\n\t\t\tstate = Defaults( layer );\r\n\r\n\t\tsettings.Layer = layer;\r\n\t\tsettings.MixCap = Math.Clamp( state.MixCap, 0.05f, SubTerrainPaintSettings.MixMaxFor( layer ) );\r\n\t\tsettings.OnlyOnSlope = state.OnlyOnSlope;\r\n\t\tsettings.SlopeDegrees = state.SlopeDegrees;\r\n\t\tsettings.MixOnly = state.MixOnly;\r\n\t\tsettings.LimitToSampledGround = state.LimitToSampledGround;\r\n\t\tsettings.PackCenter = state.PackCenter;\r\n\t\tsettings.PackCore = Math.Clamp( state.PackCore, SubTerrainPaintSettings.PackCoreMin, SubTerrainPaintSettings.PackCoreMax );\r\n\t}\r\n\r\n\tstatic LayerState Read( SubTerrainPaintSettings settings )\r\n\t{\r\n\t\treturn new LayerState\r\n\t\t{\r\n\t\t\tMixCap = settings.MixCap,\r\n\t\t\tOnlyOnSlope = settings.OnlyOnSlope,\r\n\t\t\tSlopeDegrees = settings.SlopeDegrees,\r\n\t\t\tMixOnly = settings.MixOnly,\r\n\t\t\tLimitToSampledGround = settings.LimitToSampledGround,\r\n\t\t\tPackCenter = settings.PackCenter,\r\n\t\t\tPackCore = settings.PackCore\r\n\t\t};\r\n\t}\r\n\r\n\tstatic LayerState Defaults( SubTerrainPaintLayer layer )\r\n\t{\r\n\t\tif ( layer == SubTerrainPaintLayer.Path )\r\n\t\t{\r\n\t\t\treturn new LayerState\r\n\t\t\t{\r\n\t\t\t\tMixCap = 0.36f,\r\n\t\t\t\tOnlyOnSlope = false,\r\n\t\t\t\tSlopeDegrees = 20f,\r\n\t\t\t\tPackCenter = true,\r\n\t\t\t\tPackCore = 0.55f\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tif ( layer == SubTerrainPaintLayer.Overlay )\r\n\t\t{\r\n\t\t\treturn new LayerState\r\n\t\t\t{\r\n\t\t\t\tMixCap = 0.28f,\r\n\t\t\t\tOnlyOnSlope = true,\r\n\t\t\t\tSlopeDegrees = 20f,\r\n\t\t\t\tPackCenter = true,\r\n\t\t\t\tPackCore = 0.55f\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\treturn new LayerState\r\n\t\t{\r\n\t\t\tMixCap = 0.28f,\r\n\t\t\tOnlyOnSlope = false,\r\n\t\t\tSlopeDegrees = 20f,\r\n\t\t\tPackCenter = true,\r\n\t\t\tPackCore = 0.55f\r\n\t\t};\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "TerrainSculptNet.cs",
            "FileName": "TerrainSculptNet.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\n/// <summary>\r\n/// Optional host-authoritative play-sculpt net. Attach to a NetworkMode.Object\r\n/// GameObject. Host writes height; peers apply the same op locally.\r\n/// </summary>\r\n[Title( \"Terrain Sculpt Net\" )]\r\n[Category( \"SubTerrain\" )]\r\n[Icon( \"share\" )]\r\npublic sealed class TerrainSculptNet : Component\r\n{\r\n\tpublic bool Submit( TerrainEditRequest request )\r\n\t{\r\n\t\tif ( request.Seed == 0 )\r\n\t\t\trequest.Seed = Game.Random.Int( 1, 1_000_000_000 );\r\n\r\n\t\tif ( !Networking.IsActive || Networking.IsHost )\r\n\t\t{\r\n\t\t\tApplyLocal( request );\r\n\t\t\tif ( Networking.IsActive )\r\n\t\t\t\tBroadcastStamp( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tRequestHost( (int)request.Kind, request.WorldPos, request.Radius, request.Depth, request.Seed, request.HasTargetZ, request.TargetZ );\r\n\t\treturn true;\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tpublic void RequestHost( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )\r\n\t{\r\n\t\tvar request = Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ );\r\n\t\tApplyLocal( request );\r\n\t\tBroadcastStamp( kind, pos, radius, depth, seed, hasTargetZ, targetZ );\r\n\t}\r\n\r\n\t[Rpc.Broadcast]\r\n\tpublic void BroadcastStamp( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )\r\n\t{\r\n\t\tif ( Networking.IsHost )\r\n\t\t\treturn;\r\n\r\n\t\tApplyLocal( Make( kind, pos, radius, depth, seed, hasTargetZ, targetZ ) );\r\n\t}\r\n\r\n\tstatic TerrainEditRequest Make( int kind, Vector3 pos, float radius, float depth, int seed, bool hasTargetZ, float targetZ )\r\n\t{\r\n\t\treturn new TerrainEditRequest\r\n\t\t{\r\n\t\t\tKind = (TerrainEditKind)kind,\r\n\t\t\tWorldPos = pos,\r\n\t\t\tRadius = radius,\r\n\t\t\tDepth = depth,\r\n\t\t\tSeed = seed,\r\n\t\t\tHasTargetZ = hasTargetZ,\r\n\t\t\tTargetZ = targetZ\r\n\t\t};\r\n\t}\r\n\r\n\tvoid ApplyLocal( TerrainEditRequest request )\r\n\t{\r\n\t\tTerrainSculpt.TryApply( Scene, request );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "TerrainDebrisFx.cs",
            "FileName": "TerrainDebrisFx.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\n/// <summary>\r\n/// Dirt spray and physics clods for crater / dig stamps. Listens to\r\n/// TerrainSculpt.OnApplied after GPU and collision so debris lands in the bowl.\r\n/// Prefabs use engine meshes (models/dev/box.vmdl).\r\n/// </summary>\r\n[Title( \"Terrain Debris FX\" )]\r\n[Category( \"SubTerrain\" )]\r\n[Icon( \"grain\" )]\r\npublic sealed class TerrainDebrisFx : Component\r\n{\r\n\tconst string DefaultClodPath = \"prefabs/subterrain/dirt_clod.prefab\";\r\n\tconst string DefaultSprayPath = \"prefabs/subterrain/dirt_spray.prefab\";\r\n\tconst FindMode RigidbodyFind = FindMode.Enabled | FindMode.InSelf | FindMode.InDescendants;\r\n\r\n\t[Property, Group( \"Source\" )]\r\n\tpublic TerrainSculpt Sculpt { get; set; }\r\n\r\n\t[Property, Group( \"Prefabs\" )]\r\n\tpublic PrefabFile ClodPrefab { get; set; }\r\n\r\n\t[Property, Group( \"Prefabs\" )]\r\n\tpublic PrefabFile SprayPrefab { get; set; }\r\n\r\n\t[Property, Group( \"Kinds\" )]\r\n\tpublic bool Craters { get; set; } = true;\r\n\r\n\t[Property, Group( \"Kinds\" )]\r\n\tpublic bool Digs { get; set; } = true;\r\n\r\n\t[Property, Group( \"Clods\" ), Range( 0, 48 )]\r\n\tpublic int MinClods { get; set; } = 8;\r\n\r\n\t[Property, Group( \"Clods\" ), Range( 1, 64 )]\r\n\tpublic int MaxClods { get; set; } = 24;\r\n\r\n\t[Property, Group( \"Clods\" ), Range( 0f, 0.5f )]\r\n\tpublic float ClodsPerRadius { get; set; } = 0.12f;\r\n\r\n\t[Property, Group( \"Clods\" ), Range( 0, 128 )]\r\n\tpublic int MaxAlive { get; set; } = 64;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0f, 4000f )]\r\n\tpublic float Impulse { get; set; } = 640f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0f, 3f )]\r\n\tpublic float UpBias { get; set; } = 0.45f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0.5f, 4f )]\r\n\tpublic float Outward { get; set; } = 2.2f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0.4f, 1.4f )]\r\n\tpublic float RimMin { get; set; } = 0.78f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0.5f, 1.6f )]\r\n\tpublic float RimMax { get; set; } = 1.12f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0f, 40f )]\r\n\tpublic float Spin { get; set; } = 14f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0f, 48f )]\r\n\tpublic float SpawnLift { get; set; } = 20f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0.05f, 1.5f )]\r\n\tpublic float ScaleMin { get; set; } = 0.7f;\r\n\r\n\t[Property, Group( \"Launch\" ), Range( 0.05f, 2.5f )]\r\n\tpublic float ScaleMax { get; set; } = 1.35f;\r\n\r\n\t[Property, Group( \"Dig\" ), Range( 0.1f, 1f )]\r\n\tpublic float DigImpulseScale { get; set; } = 0.5f;\r\n\r\n\t[Property, Group( \"Debug\" )]\r\n\tpublic bool DebugLog { get; set; }\r\n\r\n\treadonly List<GameObject> _alive = new();\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tif ( !Game.IsPlaying )\r\n\t\t\treturn;\r\n\r\n\t\tSculpt ??= Components.Get<TerrainSculpt>( FindMode.EnabledInSelf );\r\n\t\tClodPrefab ??= ResourceLibrary.Get<PrefabFile>( DefaultClodPath );\r\n\t\tSprayPrefab ??= ResourceLibrary.Get<PrefabFile>( DefaultSprayPath );\r\n\r\n\t\tif ( Sculpt == null || !Sculpt.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"[Debris] {GameObject.Name}: no TerrainSculpt on this object\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tSculpt.OnApplied += OnTerrainApplied;\r\n\t\tSculpt.OnReset += ClearDebris;\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tUnhook();\r\n\t\tClearDebris();\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tUnhook();\r\n\t}\r\n\r\n\tvoid Unhook()\r\n\t{\r\n\t\tif ( Sculpt == null || !Sculpt.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tSculpt.OnApplied -= OnTerrainApplied;\r\n\t\tSculpt.OnReset -= ClearDebris;\r\n\t}\r\n\r\n\tvoid OnTerrainApplied( TerrainEditResult result )\r\n\t{\r\n\t\tif ( result.Kind == TerrainEditKind.Flatten )\r\n\t\t\treturn;\r\n\r\n\t\tif ( result.Kind == TerrainEditKind.Crater && !Craters )\r\n\t\t\treturn;\r\n\r\n\t\tif ( result.Kind == TerrainEditKind.Dig && !Digs )\r\n\t\t\treturn;\r\n\r\n\t\tSpawnSpray( result );\r\n\t\tSpawnClods( result );\r\n\t}\r\n\r\n\tvoid SpawnSpray( TerrainEditResult result )\r\n\t{\r\n\t\tif ( SprayPrefab == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar aimUp = Rotation.LookAt( Vector3.Up, Vector3.Forward );\r\n\t\tvar spray = GameObject.Clone( SprayPrefab, new Transform( result.Origin, aimUp ) );\r\n\t\tif ( !spray.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tTrack( spray );\r\n\t}\r\n\r\n\tvoid SpawnClods( TerrainEditResult result )\r\n\t{\r\n\t\tif ( ClodPrefab == null )\r\n\t\t\treturn;\r\n\r\n\t\tvar count = (int)Math.Clamp( result.Radius * ClodsPerRadius, MinClods, MaxClods );\r\n\t\tif ( result.Kind == TerrainEditKind.Dig )\r\n\t\t\tcount = Math.Max( 3, count / 2 );\r\n\r\n\t\tvar impulse = Impulse;\r\n\t\tvar up = UpBias;\r\n\t\tif ( result.Kind == TerrainEditKind.Dig )\r\n\t\t{\r\n\t\t\timpulse *= DigImpulseScale;\r\n\t\t\tup *= 0.65f;\r\n\t\t}\r\n\r\n\t\tfor ( var i = 0; i < count; i++ )\r\n\t\t\tSpawnClod( result, impulse, up, i, count );\r\n\r\n\t\tDbg( $\"spawned {count} clods kind={result.Kind} r={result.Radius:0.#}\" );\r\n\t}\r\n\r\n\tvoid SpawnClod( TerrainEditResult result, float impulse, float up, int index, int count )\r\n\t{\r\n\t\tvar yaw = (360f * index / Math.Max( count, 1 )) + Random.Shared.Float( -14f, 14f );\r\n\t\tvar outward = Rotation.FromYaw( yaw ).Forward;\r\n\t\tvar rimMin = result.Kind == TerrainEditKind.Dig\r\n\t\t\t? Math.Clamp( RimMin * 0.7f, 0.4f, 1.2f )\r\n\t\t\t: Math.Clamp( RimMin, 0.4f, 1.4f );\r\n\t\tvar rimMax = Math.Max( rimMin + 0.05f, RimMax );\r\n\t\tvar radial = result.Radius * Random.Shared.Float( rimMin, rimMax );\r\n\t\tvar pos = result.Origin + outward * radial + Vector3.Up * SpawnLift;\r\n\t\tvar scale = Random.Shared.Float( ScaleMin, ScaleMax );\r\n\t\tvar rot = Rotation.From( new Angles(\r\n\t\t\tRandom.Shared.Float( 0f, 360f ),\r\n\t\t\tRandom.Shared.Float( 0f, 360f ),\r\n\t\t\tRandom.Shared.Float( 0f, 360f )\r\n\t\t) );\r\n\r\n\t\tvar clod = GameObject.Clone( ClodPrefab, new Transform( pos, rot ) );\r\n\t\tif ( !clod.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tclod.WorldScale *= scale;\r\n\r\n\t\tTrack( clod );\r\n\r\n\t\tvar body = clod.Components.Get<Rigidbody>( RigidbodyFind );\r\n\t\tif ( !body.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar dir = (outward * Outward + Vector3.Up * up).Normal;\r\n\t\tvar speed = impulse * Random.Shared.Float( 0.85f, 1.25f );\r\n\t\tbody.Sleeping = false;\r\n\t\tbody.Velocity = dir * speed;\r\n\t\tbody.AngularVelocity = new Vector3(\r\n\t\t\tRandom.Shared.Float( -1f, 1f ),\r\n\t\t\tRandom.Shared.Float( -1f, 1f ),\r\n\t\t\tRandom.Shared.Float( -1f, 1f )\r\n\t\t) * Spin;\r\n\t}\r\n\r\n\tvoid Track( GameObject go )\r\n\t{\r\n\t\tPrune();\r\n\t\t_alive.Add( go );\r\n\r\n\t\twhile ( MaxAlive > 0 && _alive.Count > MaxAlive )\r\n\t\t{\r\n\t\t\tvar oldest = _alive[0];\r\n\t\t\t_alive.RemoveAt( 0 );\r\n\t\t\tif ( oldest.IsValid() )\r\n\t\t\t\toldest.Destroy();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid ClearDebris()\r\n\t{\r\n\t\tfor ( var i = _alive.Count - 1; i >= 0; i-- )\r\n\t\t{\r\n\t\t\tif ( _alive[i].IsValid() )\r\n\t\t\t\t_alive[i].Destroy();\r\n\t\t}\r\n\r\n\t\t_alive.Clear();\r\n\t}\r\n\r\n\tvoid Prune()\r\n\t{\r\n\t\tfor ( var i = _alive.Count - 1; i >= 0; i-- )\r\n\t\t{\r\n\t\t\tif ( !_alive[i].IsValid() )\r\n\t\t\t\t_alive.RemoveAt( i );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Dbg( string message )\r\n\t{\r\n\t\tif ( !DebugLog )\r\n\t\t\treturn;\r\n\r\n\t\tLog.Info( $\"[Debris] {message}\" );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/SubTerrainBaseBrushTool.cs",
            "FileName": "SubTerrainBaseBrushTool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing Sandbox;\r\nusing Editor.TerrainEditor;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\npublic struct SubTerrainPaintParameters\r\n{\r\n\tpublic Vector3 HitPosition { get; set; }\r\n\tpublic Vector2 HitUV { get; set; }\r\n\tpublic float FlattenHeight { get; set; }\r\n\tpublic SubTerrainBrushSettings BrushSettings { get; set; }\r\n}\r\n\r\n/// <summary>\r\n/// Shared stroke logic for SubTerrain sculpt tools.\r\n/// </summary>\r\npublic abstract class SubTerrainBaseBrushTool : EditorTool\r\n{\r\n\tprotected SubTerrainTool _parent;\r\n\tprotected bool _dragging;\r\n\tprotected RectInt _dirtyRegion;\r\n\r\n\tprotected SubTerrainSculptMode Mode { get; set; }\r\n\tprotected Vector2 SlopeStartUV;\r\n\tprotected float SlopeStartHeight01;\r\n\tprotected bool AllowBrushInvert { get; set; }\r\n\tprotected bool AllowHeightSample { get; set; }\r\n\tprotected Plane StrokePlane;\r\n\tbool _samplingHeight;\r\n\r\n\tGpuBuffer<BrushData> _engineBrushBuffer;\r\n\r\n\tVector3 _lastHitWorldPos;\r\n\tVector3 _lastPaintLocal;\r\n\tTransform _lastHitTx;\r\n\tVector2? _cursorLockPosition;\r\n\tfloat _raiseStampAccum;\r\n\r\n\tpublic SubTerrainBaseBrushTool( SubTerrainTool parent )\r\n\t{\r\n\t\t_parent = parent;\r\n\t}\r\n\r\n\tpublic override void OnEnabled()\r\n\t{\r\n\t\tAllowGameObjectSelection = false;\r\n\t\t_parent.SetBrushMode( GetType().Name );\r\n\t}\r\n\r\n\tpublic override void OnDisabled()\r\n\t{\r\n\t\t_parent.DetachSidebarForReuse();\r\n\t}\r\n\r\n\tpublic virtual bool GetHitPosition( Terrain terrain, out Vector3 position )\r\n\t{\r\n\t\treturn terrain.RayIntersects( Gizmo.CurrentRay, Gizmo.RayDepth, out position );\r\n\t}\r\n\r\n\tpublic override void OnUpdate()\r\n\t{\r\n\t\tvar terrain = GetSelectedComponent<Terrain>();\r\n\t\tif ( !terrain.IsValid() )\r\n\t\t{\r\n\t\t\t_parent.ClearStatus();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !GetHitPosition( terrain, out var hitPosition ) )\r\n\t\t{\r\n\t\t\t_parent.ClearStatus();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar tx = terrain.WorldTransform;\r\n\t\t_parent.UpdateStatus( terrain, hitPosition );\r\n\r\n\t\tif ( Application.MouseButtons.HasFlag( MouseButtons.Middle ) )\r\n\t\t{\r\n\t\t\t_cursorLockPosition ??= Application.UnscaledCursorPosition;\r\n\t\t\tvar d = Application.UnscaledCursorPosition - _cursorLockPosition.Value;\r\n\t\t\tvar brush = _parent.BrushSettings;\r\n\r\n\t\t\tif ( Gizmo.IsShiftPressed )\r\n\t\t\t{\r\n\t\t\t\tbrush.Size = SubTerrainBrushSize.Clamp( (int)(brush.Size + d.x * 0.25f) );\r\n\t\t\t\tbrush.Opacity = Math.Clamp( brush.Opacity - d.y * 0.002f, 0f, 1f );\r\n\t\t\t\t_parent.SyncBrushSizeWidget();\r\n\t\t\t}\r\n\t\t\telse if ( Gizmo.IsCtrlPressed && !brush.RandomRotation )\r\n\t\t\t{\r\n\t\t\t\tbrush.Rotation = ((brush.Rotation + d.x * 0.5f) % 360f + 360f) % 360f;\r\n\t\t\t\t_parent.SyncBrushRotationRows();\r\n\t\t\t}\r\n\r\n\t\t\tApplication.UnscaledCursorPosition = _cursorLockPosition.Value;\r\n\t\t\tSceneOverlay.Parent.Cursor = CursorShape.Blank;\r\n\r\n\t\t\tDrawBrushAdjustText();\r\n\t\t\t_parent.DrawBrushPreview( new Transform( _lastHitWorldPos, _lastHitTx.Rotation ), terrain );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _cursorLockPosition.HasValue )\r\n\t\t{\r\n\t\t\tSceneOverlay.Parent.Cursor = CursorShape.None;\r\n\t\t\t_cursorLockPosition = null;\r\n\t\t}\r\n\r\n\t\t_lastHitWorldPos = tx.PointToWorld( hitPosition );\r\n\t\t_lastHitTx = tx;\r\n\r\n\t\tif ( AllowHeightSample && Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Alt ) )\r\n\t\t{\r\n\t\t\tif ( Gizmo.IsLeftMouseDown )\r\n\t\t\t{\r\n\t\t\t\tif ( !_samplingHeight )\r\n\t\t\t\t{\r\n\t\t\t\t\t_samplingHeight = true;\r\n\t\t\t\t\t_parent.BrushSettings.TargetHeight = hitPosition.z;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t_samplingHeight = false;\r\n\t\t\t}\r\n\r\n\t\t\t_parent.DrawBrushPreview( new Transform( _lastHitWorldPos, tx.Rotation ), terrain );\r\n\t\t\tDrawToolOverlay( terrain, _lastHitWorldPos );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_samplingHeight = false;\r\n\r\n\t\tif ( Gizmo.IsLeftMouseDown )\r\n\t\t{\r\n\t\t\tbool moved = !Application.CursorDelta.IsNearZeroLength;\r\n\t\t\tbool shouldSculpt = !_dragging || moved;\r\n\r\n\t\t\tif ( !_dragging )\r\n\t\t\t{\r\n\t\t\t\tif ( _parent.BrushSettings.RandomRotation )\r\n\t\t\t\t\t_parent.BrushSettings.Rotation = Random.Shared.NextSingle() * 360f;\r\n\r\n\t\t\t\tStrokePlane = new Plane( _lastHitWorldPos, tx.Rotation.Up );\r\n\t\t\t\t_dragging = true;\r\n\t\t\t\t_raiseStampAccum = 0f;\r\n\r\n\t\t\t\tvar uv = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize;\r\n\t\t\t\tvar x = (int)Math.Floor( terrain.Storage.Resolution * uv.x );\r\n\t\t\t\tvar y = (int)Math.Floor( terrain.Storage.Resolution * uv.y );\r\n\t\t\t\t_dirtyRegion = new( new Vector2Int( x, y ) );\r\n\t\t\t\t_lastPaintLocal = hitPosition;\r\n\r\n\t\t\t\tOnStrokeStart( terrain, hitPosition );\r\n\t\t\t\tOnPaint( terrain, MakePaintParams( hitPosition, terrain ) );\r\n\t\t\t}\r\n\t\t\telse if ( Mode == SubTerrainSculptMode.RaiseLower )\r\n\t\t\t{\r\n\t\t\t\tif ( moved )\r\n\t\t\t\t{\r\n\t\t\t\t\tStrokeSculpt( terrain, hitPosition );\r\n\t\t\t\t\t_raiseStampAccum = 0f;\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\t// Hold in place: keep raising/lowering at Speed stamps per second.\r\n\t\t\t\t\tfloat t = Math.Clamp( (_parent.BrushSettings.Speed - 1) / 99f, 0f, 1f );\r\n\t\t\t\t\tfloat stampsPerSec = 2f + t * 58f;\r\n\t\t\t\t\t_raiseStampAccum += Math.Clamp( RealTime.Delta, 0f, 0.1f ) * stampsPerSec;\r\n\t\t\t\t\twhile ( _raiseStampAccum >= 1f )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t_raiseStampAccum -= 1f;\r\n\t\t\t\t\t\tOnPaint( terrain, MakePaintParams( hitPosition, terrain ) );\r\n\t\t\t\t\t\t_lastPaintLocal = hitPosition;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse if ( shouldSculpt )\r\n\t\t\t{\r\n\t\t\t\tStrokeSculpt( terrain, hitPosition );\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( _dragging )\r\n\t\t{\r\n\t\t\t_dragging = false;\r\n\t\t\t_raiseStampAccum = 0f;\r\n\t\t\tOnPaintEnded( terrain );\r\n\t\t}\r\n\r\n\t\t_parent.DrawBrushPreview( new Transform( _lastHitWorldPos, tx.Rotation ), terrain );\r\n\t\tDrawToolOverlay( terrain, _lastHitWorldPos );\r\n\t}\r\n\r\n\tSubTerrainPaintParameters MakePaintParams( Vector3 hitPosition, Terrain terrain )\r\n\t{\r\n\t\treturn new SubTerrainPaintParameters\r\n\t\t{\r\n\t\t\tHitPosition = hitPosition,\r\n\t\t\tHitUV = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize,\r\n\t\t\tFlattenHeight = hitPosition.z / terrain.Storage.TerrainHeight,\r\n\t\t\tBrushSettings = _parent.BrushSettings\r\n\t\t};\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// World-space stamp fill so fast / zoomed-out drags do not leave dashed strokes.\r\n\t/// </summary>\r\n\tvoid StrokeSculpt( Terrain terrain, Vector3 hitLocal )\r\n\t{\r\n\t\tvar from = _lastPaintLocal;\r\n\t\tvar to = hitLocal;\r\n\t\tfloat dist = (to - from).Length;\r\n\t\tfloat step = Math.Max( _parent.BrushSettings.Size * 0.2f, 8f );\r\n\r\n\t\tif ( dist < 0.001f )\r\n\t\t\treturn;\r\n\r\n\t\tif ( dist <= step )\r\n\t\t{\r\n\t\t\tOnPaint( terrain, MakePaintParams( to, terrain ) );\r\n\t\t\t_lastPaintLocal = to;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tconst int maxStamps = 64;\r\n\t\tint stamps = (int)Math.Ceiling( dist / step );\r\n\t\tif ( stamps > maxStamps )\r\n\t\t{\r\n\t\t\tstamps = maxStamps;\r\n\t\t\tstep = dist / stamps;\r\n\t\t}\r\n\r\n\t\tfor ( int i = 1; i <= stamps; i++ )\r\n\t\t{\r\n\t\t\tfloat t = i / (float)stamps;\r\n\t\t\tOnPaint( terrain, MakePaintParams( Vector3.Lerp( from, to, t ), terrain ) );\r\n\t\t}\r\n\r\n\t\t_lastPaintLocal = to;\r\n\t}\r\n\r\n\tvoid DrawBrushAdjustText()\r\n\t{\r\n\t\tvar textScope = new TextRendering.Scope\r\n\t\t{\r\n\t\t\tTextColor = Color.White,\r\n\t\t\tFontSize = 16 * Gizmo.Settings.GizmoScale * Application.DpiScale,\r\n\t\t\tFontName = \"Roboto Mono\",\r\n\t\t\tFontWeight = 600,\r\n\t\t\tLineHeight = 1,\r\n\t\t\tOutline = new TextRendering.Outline() { Color = Color.Black, Enabled = true, Size = 3 }\r\n\t\t};\r\n\r\n\t\tvar offset = Vector2.Up * 24;\r\n\t\tvar brush = _parent.BrushSettings;\r\n\r\n\t\tif ( Gizmo.IsShiftPressed )\r\n\t\t{\r\n\t\t\ttextScope.Text = $\"Size: {brush.Size}\";\r\n\t\t\tGizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset );\r\n\r\n\t\t\ttextScope.Text = $\"Opacity: {brush.Opacity:0.##}\";\r\n\t\t\tGizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset * 2 );\r\n\r\n\t\t\tif ( Mode == SubTerrainSculptMode.RaiseLower )\r\n\t\t\t{\r\n\t\t\t\ttextScope.Text = $\"Speed: {brush.Speed}\";\r\n\t\t\t\tGizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset * 3 );\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if ( Gizmo.IsCtrlPressed && !brush.RandomRotation )\r\n\t\t{\r\n\t\t\ttextScope.Text = $\"Rotation: {brush.Rotation:0.#}\";\r\n\t\t\tGizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected virtual void DrawToolOverlay( Terrain terrain, Vector3 worldCenter )\r\n\t{\r\n\t}\r\n\r\n\tprotected virtual void OnStrokeStart( Terrain terrain, Vector3 hitPosition )\r\n\t{\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// World Size is brush radius; dispatch covers the diameter (Size * 2 in world units).\r\n\t/// </summary>\r\n\tprotected int BrushTexelSize( Terrain terrain, float worldSize )\r\n\t{\r\n\t\tint size = (int)Math.Floor( worldSize * 2.0f / terrain.Storage.TerrainSize * terrain.Storage.Resolution );\r\n\t\treturn Math.Max( size, 1 );\r\n\t}\r\n\r\n\tprotected void GrowDirtyRegion( Terrain terrain, Vector2 hitUV, int size )\r\n\t{\r\n\t\tvar x = (int)Math.Floor( terrain.Storage.Resolution * hitUV.x ) - size / 2;\r\n\t\tvar y = (int)Math.Floor( terrain.Storage.Resolution * hitUV.y ) - size / 2;\r\n\t\t_dirtyRegion.Add( new RectInt( x, y, size + 1, size + 1 ) );\r\n\t}\r\n\r\n\tprotected virtual void OnPaint( Terrain terrain, SubTerrainPaintParameters paint )\r\n\t{\r\n\t\tvar brushTexture = _parent.SelectedBrushTexture;\r\n\t\tif ( brushTexture is null )\r\n\t\t\treturn;\r\n\r\n\t\tint size = BrushTexelSize( terrain, paint.BrushSettings.Size );\r\n\t\tvar amount = paint.BrushSettings.Opacity;\r\n\t\tif ( Mode == SubTerrainSculptMode.RaiseLower )\r\n\t\t{\r\n\t\t\t// Speed 50 = historical 1x height per stamp; 100 \u2248 4x. Hold rate is separate.\r\n\t\t\tamount *= Math.Clamp( paint.BrushSettings.Speed / 50f, 0.02f, 4f );\r\n\t\t}\r\n\r\n\t\tvar strength = amount * (AllowBrushInvert && Gizmo.IsCtrlPressed ? -1.0f : 1.0f);\r\n\r\n\t\tDispatchSculpt( terrain, paint, brushTexture, size, strength );\r\n\t}\r\n\r\n\tprotected void DispatchSculpt( Terrain terrain, SubTerrainPaintParameters paint, Texture brushTexture, int size, float strength )\r\n\t{\r\n\t\tif ( (int)Mode <= (int)SubTerrainSculptMode.Noise )\r\n\t\t{\r\n\t\t\tDispatchEngineSculpt( terrain, paint, brushTexture, size, strength );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar cs = new ComputeShader( \"terrain/cs_subterrain_sculpt\" );\r\n\r\n\t\tcs.Attributes.SetComboEnum( \"D_SCULPT_MODE\", Mode );\r\n\r\n\t\tcs.Attributes.Set( \"Heightmap\", terrain.HeightMap );\r\n\t\tcs.Attributes.Set( \"ControlMap\", terrain.ControlMap );\r\n\r\n\t\tcs.Attributes.Set( \"HeightUV\", paint.HitUV );\r\n\t\tcs.Attributes.Set( \"FlattenHeight\", paint.FlattenHeight );\r\n\t\tcs.Attributes.Set( \"SlopeStartUV\", SlopeStartUV );\r\n\t\tcs.Attributes.Set( \"SlopeEndUV\", paint.HitUV );\r\n\t\tcs.Attributes.Set( \"SlopeStartHeight\", SlopeStartHeight01 );\r\n\t\tcs.Attributes.Set( \"SlopeEndHeight\", paint.FlattenHeight );\r\n\t\tcs.Attributes.Set( \"BrushStrength\", strength );\r\n\t\tcs.Attributes.Set( \"BrushSize\", size );\r\n\t\tcs.Attributes.Set( \"Brush\", brushTexture );\r\n\r\n\t\tApplyExtraAttributes( cs, paint );\r\n\r\n\t\tcs.Dispatch( size, size, 1 );\r\n\r\n\t\tGrowDirtyRegion( terrain, paint.HitUV, size );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Modes 0-4 match the engine sculpt shader, which already supports brush rotation.\r\n\t/// Avoids recompiling cs_subterrain_sculpt (that compile has been crashing the editor).\r\n\t/// </summary>\r\n\tvoid DispatchEngineSculpt( Terrain terrain, SubTerrainPaintParameters paint, Texture brushTexture, int size, float strength )\r\n\t{\r\n\t\tvar cs = new ComputeShader( \"terrain/cs_terrain_sculpt\" );\r\n\r\n\t\tcs.Attributes.SetComboEnum( \"D_SCULPT_MODE\", (SculptMode)(int)Mode );\r\n\r\n\t\tcs.Attributes.Set( \"Heightmap\", terrain.HeightMap );\r\n\t\tcs.Attributes.Set( \"ControlMap\", terrain.ControlMap );\r\n\r\n\t\t_engineBrushBuffer ??= new GpuBuffer<BrushData>( 1 );\r\n\t\t_engineBrushBuffer.SetData( new[]\r\n\t\t{\r\n\t\t\tnew BrushData\r\n\t\t\t{\r\n\t\t\t\tUV = paint.HitUV,\r\n\t\t\t\tStrength = strength,\r\n\t\t\t\tSize = size,\r\n\t\t\t\tRotation = paint.BrushSettings.Rotation * MathF.PI / 180f,\r\n\t\t\t\tFlattenHeight = paint.FlattenHeight,\r\n\t\t\t}\r\n\t\t} );\r\n\t\tcs.Attributes.Set( \"BrushSettings\", _engineBrushBuffer );\r\n\t\tcs.Attributes.Set( \"Brush\", brushTexture );\r\n\r\n\t\tcs.Dispatch( size, size, 1 );\r\n\r\n\t\tGrowDirtyRegion( terrain, paint.HitUV, size );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Set any attributes a sub-tool needs on top of the shared sculpt ones.\r\n\t/// Runs after the shared attributes, so it can override them.\r\n\t/// </summary>\r\n\tprotected virtual void ApplyExtraAttributes( ComputeShader cs, SubTerrainPaintParameters paint )\r\n\t{\r\n\t}\r\n\r\n\tprivate static T[] CopyRegion<T>( T[] data, int stride, RectInt rect ) where T : unmanaged\r\n\t{\r\n\t\tT[] region = new T[rect.Width * rect.Height];\r\n\r\n\t\tfor ( int y = 0; y < rect.Height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < rect.Width; x++ )\r\n\t\t\t{\r\n\t\t\t\tregion[x + y * rect.Width] = data[rect.Left + x + (rect.Top + y) * stride];\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn region;\r\n\t}\r\n\r\n\tprivate static Action CreateUndoAction<T>( Terrain terrain, T[] dest, T[] region, RectInt dirtyRegion, Terrain.SyncFlags flags ) => () =>\r\n\t{\r\n\t\tif ( !terrain.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tfor ( int y = 0; y < dirtyRegion.Height; y++ )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x < dirtyRegion.Width; x++ )\r\n\t\t\t{\r\n\t\t\t\tdest[dirtyRegion.Left + x + (dirtyRegion.Top + y) * terrain.Storage.Resolution] = region[x + y * dirtyRegion.Width];\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tterrain.SyncGPUTexture();\r\n\t\tterrain.UpdateCollision( flags, dirtyRegion );\r\n\t};\r\n\r\n\tprotected virtual void OnPaintEnded( Terrain terrain )\r\n\t{\r\n\t\t_dirtyRegion.Left = Math.Clamp( _dirtyRegion.Left, 0, terrain.Storage.Resolution - 1 );\r\n\t\t_dirtyRegion.Right = Math.Clamp( _dirtyRegion.Right, 0, terrain.Storage.Resolution - 1 );\r\n\t\t_dirtyRegion.Top = Math.Clamp( _dirtyRegion.Top, 0, terrain.Storage.Resolution - 1 );\r\n\t\t_dirtyRegion.Bottom = Math.Clamp( _dirtyRegion.Bottom, 0, terrain.Storage.Resolution - 1 );\r\n\r\n\t\tvar name = $\"Terrain {DisplayInfo.For( this ).Name}\";\r\n\r\n\t\tif ( Mode != SubTerrainSculptMode.Hole )\r\n\t\t{\r\n\t\t\tvar regionBefore = CopyRegion( terrain.Storage.HeightMap, terrain.Storage.Resolution, _dirtyRegion );\r\n\t\t\tterrain.SyncCPUTexture( Terrain.SyncFlags.Height, _dirtyRegion );\r\n\t\t\tvar regionAfter = CopyRegion( terrain.Storage.HeightMap, terrain.Storage.Resolution, _dirtyRegion );\r\n\r\n\t\t\tSceneEditorSession.Active.UndoSystem.Insert( name,\r\n\t\t\t\tCreateUndoAction( terrain, terrain.Storage.HeightMap, regionBefore, _dirtyRegion, Terrain.SyncFlags.Height ),\r\n\t\t\t\tCreateUndoAction( terrain, terrain.Storage.HeightMap, regionAfter, _dirtyRegion, Terrain.SyncFlags.Height ) );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar regionBefore = CopyRegion( terrain.Storage.ControlMap, terrain.Storage.Resolution, _dirtyRegion );\r\n\t\t\tterrain.SyncCPUTexture( Terrain.SyncFlags.Control, _dirtyRegion );\r\n\t\t\tvar regionAfter = CopyRegion( terrain.Storage.ControlMap, terrain.Storage.Resolution, _dirtyRegion );\r\n\r\n\t\t\tSceneEditorSession.Active.UndoSystem.Insert( name,\r\n\t\t\t\tCreateUndoAction( terrain, terrain.Storage.ControlMap, regionBefore, _dirtyRegion, Terrain.SyncFlags.Control ),\r\n\t\t\t\tCreateUndoAction( terrain, terrain.Storage.ControlMap, regionAfter, _dirtyRegion, Terrain.SyncFlags.Control ) );\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "TimedDestroy.cs",
            "FileName": "TimedDestroy.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\n/// <summary>\r\n/// Destroys this GameObject after a delay. Used by debris prefabs.\r\n/// </summary>\r\n[Title( \"Timed Destroy\" )]\r\n[Category( \"SubTerrain\" )]\r\n[Icon( \"timer\" )]\r\npublic sealed class TimedDestroy : Component\r\n{\r\n\t[Property] public float Seconds { get; set; } = 12f;\r\n\r\n\tTimeSince _alive;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_alive = 0;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( _alive >= Seconds )\r\n\t\t\tGameObject.Destroy();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Code/TimedDestroy.cs",
            "FileName": "TimedDestroy.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing Sandbox;\r\n\r\nnamespace SubZero.SubTerrain;\r\n\r\n/// <summary>\r\n/// Destroys this GameObject after a delay. Used by debris prefabs.\r\n/// </summary>\r\n[Title( \"Timed Destroy\" )]\r\n[Category( \"SubTerrain\" )]\r\n[Icon( \"timer\" )]\r\npublic sealed class TimedDestroy : Component\r\n{\r\n\t[Property] public float Seconds { get; set; } = 12f;\r\n\r\n\tTimeSince _alive;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_alive = 0;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( _alive >= Seconds )\r\n\t\t\tGameObject.Destroy();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/Tools/PathTool.cs",
            "FileName": "PathTool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing Sandbox;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// Keeps the click height along the stroke. Paint Texture Path is the control-map path.\r\n/// </summary>\r\n[Title( \"Path\" )]\r\n[Icon( \"route\" )]\r\n[Alias( \"subterrain_path\" )]\r\n[Group( \"1\" )]\r\n[Order( 6 )]\r\npublic class SubPathTool : SubTerrainBaseBrushTool\r\n{\r\n\tVector3 _startLocal;\r\n\r\n\tpublic SubPathTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.Path;\r\n\t}\r\n\r\n\tprotected override void OnStrokeStart( Terrain terrain, Vector3 hitPosition )\r\n\t{\r\n\t\t_startLocal = hitPosition;\r\n\t\tSlopeStartUV = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize;\r\n\t\tSlopeStartHeight01 = hitPosition.z / terrain.Storage.TerrainHeight;\r\n\t}\r\n\r\n\tprotected override void DrawToolOverlay( Terrain terrain, Vector3 worldCenter )\r\n\t{\r\n\t\tif ( !_dragging )\r\n\t\t\treturn;\r\n\r\n\t\tvar startWorld = terrain.WorldTransform.PointToWorld( _startLocal );\r\n\t\tGizmo.Draw.Color = Color.FromBytes( 120, 200, 250 );\r\n\t\tGizmo.Draw.Line( startWorld, worldCenter );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/SubTerrainBrushSize.cs",
            "FileName": "SubTerrainBrushSize.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// Brush Size limits and piecewise track mapping.\r\n/// Fine band stays precise; larger sizes take less track and bigger nudges.\r\n/// </summary>\r\npublic static class SubTerrainBrushSize\r\n{\r\n\tpublic const int Min = 8;\r\n\tpublic const int FineMax = 3500;\r\n\tpublic const int MidMax = 18000;\r\n\tpublic const int Max = 35000;\r\n\r\n\tpublic const int FineStep = 50;\r\n\tpublic const int MidStep = 500;\r\n\tpublic const int CoarseStep = 1000;\r\n\r\n\t/// <summary>Fraction of the slider track for the fine band (8..FineMax).</summary>\r\n\tpublic const float FineTrack = 0.70f;\r\n\r\n\t/// <summary>End of mid band on the track (FineMax..MidMax). Coarse is the rest to 1.</summary>\r\n\tpublic const float MidTrack = 0.90f;\r\n\r\n\tpublic static int Clamp( int size ) => Math.Clamp( size, Min, Max );\r\n\r\n\tpublic static int StepFor( int size )\r\n\t{\r\n\t\tif ( size < FineMax )\r\n\t\t\treturn FineStep;\r\n\t\tif ( size < MidMax )\r\n\t\t\treturn MidStep;\r\n\t\treturn CoarseStep;\r\n\t}\r\n\r\n\tpublic static int Nudge( int size, int direction )\r\n\t{\r\n\t\tif ( direction == 0 )\r\n\t\t\treturn Clamp( size );\r\n\r\n\t\tsize = Clamp( size );\r\n\t\tint step = StepFor( size );\r\n\t\treturn Clamp( size + direction * step );\r\n\t}\r\n\r\n\tpublic static int SizeFromTrack( float t01 )\r\n\t{\r\n\t\tt01 = Math.Clamp( t01, 0f, 1f );\r\n\r\n\t\tint raw;\r\n\t\tif ( t01 <= FineTrack )\r\n\t\t{\r\n\t\t\tfloat u = FineTrack <= 0f ? 0f : t01 / FineTrack;\r\n\t\t\traw = (int)MathF.Round( Lerp( Min, FineMax, u ) );\r\n\t\t}\r\n\t\telse if ( t01 <= MidTrack )\r\n\t\t{\r\n\t\t\tfloat u = (t01 - FineTrack) / (MidTrack - FineTrack);\r\n\t\t\traw = (int)MathF.Round( Lerp( FineMax, MidMax, u ) );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tfloat u = (t01 - MidTrack) / (1f - MidTrack);\r\n\t\t\traw = (int)MathF.Round( Lerp( MidMax, Max, u ) );\r\n\t\t}\r\n\r\n\t\treturn Snap( raw );\r\n\t}\r\n\r\n\tpublic static float TrackFromSize( int size )\r\n\t{\r\n\t\tsize = Clamp( size );\r\n\r\n\t\tif ( size <= FineMax )\r\n\t\t{\r\n\t\t\tfloat u = (size - Min) / (float)(FineMax - Min);\r\n\t\t\treturn u * FineTrack;\r\n\t\t}\r\n\r\n\t\tif ( size <= MidMax )\r\n\t\t{\r\n\t\t\tfloat u = (size - FineMax) / (float)(MidMax - FineMax);\r\n\t\t\treturn FineTrack + u * (MidTrack - FineTrack);\r\n\t\t}\r\n\r\n\t\t{\r\n\t\t\tfloat u = (size - MidMax) / (float)(Max - MidMax);\r\n\t\t\treturn MidTrack + u * (1f - MidTrack);\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static int Snap( int size )\r\n\t{\r\n\t\tsize = Clamp( size );\r\n\t\tint step = StepFor( size );\r\n\t\tint snapped = (int)MathF.Round( size / (float)step ) * step;\r\n\t\treturn Clamp( snapped );\r\n\t}\r\n\r\n\tstatic float Lerp( float a, float b, float t ) => a + (b - a) * Math.Clamp( t, 0f, 1f );\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/SubTerrainTool.cs",
            "FileName": "SubTerrainTool.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\nusing Editor.TerrainEditor;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// SubTerrain editor tool. Extends the built-in terrain workflow with larger brushes,\r\n/// slope/path/set-height sculpt modes, and paint texture support.\r\n/// </summary>\r\n[EditorTool( \"tools.subterrain\" )]\r\n[Title( \"SubTerrain\" )]\r\n[Icon( \"terrain\" )]\r\n[Alias( \"subterrain\" )]\r\n[Group( \"Scene\" )]\r\npublic class SubTerrainTool : EditorTool\r\n{\r\n\tpublic static SubTerrainTool ActiveInstance { get; private set; }\r\n\r\n\tpublic SubTerrainBrushSettings BrushSettings { get; private set; } = SubTerrainBrushStore.ForMode( nameof( SubRaiseLowerTool ) );\r\n\tpublic SubTerrainPaintSettings PaintSettings { get; private set; } = new();\r\n\tpublic bool PaintArmed { get; set; }\r\n\tstring _brushMode = nameof( SubRaiseLowerTool );\r\n\r\n\tBrushPreviewSceneObject _previewObject;\r\n\tSubTerrainStatusWidget _statusWidget;\r\n\tWidget _opacityWidget;\r\n\tWidget _speedWidget;\r\n\tWidget _rotationWidget;\r\n\tWidget _brushSheetHost;\r\n\tSubTerrainBrushSizeWidget _sizeWidget;\r\n\tWidget _sidebar;\r\n\tWidget _textureSection;\r\n\tWidget _statusSection;\r\n\tSubTerrainBlendWidget _blendWidget;\r\n\tLabel _intentLabel;\r\n\r\n\tpublic Terrain PeekTerrain()\r\n\t{\r\n\t\tif ( StatusTerrain.IsValid() )\r\n\t\t\treturn StatusTerrain;\r\n\r\n\t\treturn GetSelectedComponent<Terrain>();\r\n\t}\r\n\r\n\tpublic void SetBrushMode( string key )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( key ) )\r\n\t\t\tkey = nameof( SubRaiseLowerTool );\r\n\r\n\t\tvar next = SubTerrainBrushStore.ForMode( key );\r\n\t\tbool same = _brushMode == key && BrushSettings == next;\r\n\t\t_brushMode = key;\r\n\t\tBrushSettings = next;\r\n\r\n\t\tif ( !same )\r\n\t\t{\r\n\t\t\tRebuildBrushSheet();\r\n\t\t\tUpdateIntent();\r\n\t\t}\r\n\r\n\t\tUpdateOpacityState();\r\n\t}\r\n\r\n\tpublic void RefreshPaintChrome()\r\n\t{\r\n\t\tif ( _textureSection is not null )\r\n\t\t\t_textureSection.Visible = PaintArmed;\r\n\r\n\t\tif ( _blendWidget is not null )\r\n\t\t\t_blendWidget.Visible = PaintArmed;\r\n\r\n\t\tif ( _statusSection is not null )\r\n\t\t\t_statusSection.Visible = !PaintArmed;\r\n\r\n\t\tUpdateOpacityState();\r\n\t\tUpdateIntent();\r\n\t}\r\n\r\n\tpublic void UpdateIntent()\r\n\t{\r\n\t\tif ( _intentLabel is null )\r\n\t\t\treturn;\r\n\r\n\t\tif ( PaintArmed )\r\n\t\t{\r\n\t\t\t_intentLabel.Text = \"Paint Texture.\";\r\n\t\t\t_intentLabel.ToolTip = \"Alt sample | Ctrl fade | Shift smooth | ring yellow/green = Alt/Shift. Modes and size keys are in Shortcuts below.\";\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_intentLabel.Text = IntentForSculpt();\r\n\t\t_intentLabel.ToolTip = IntentTooltipForSculpt();\r\n\t}\r\n\r\n\tstring IntentForSculpt()\r\n\t{\r\n\t\treturn _brushMode switch\r\n\t\t{\r\n\t\t\tnameof( SubSmoothTool ) => \"Smooth height.\",\r\n\t\t\tnameof( SubPlainerTool ) => \"Plainer: broad weighted leveling.\",\r\n\t\t\tnameof( SubFlattenTool ) => \"Flatten to click Z.\",\r\n\t\t\tnameof( SubSlopeTool ) => \"Slope: click start, drag to end.\",\r\n\t\t\tnameof( SubPathTool ) => \"Path: constant elevation strip.\",\r\n\t\t\tnameof( SubSetHeightTool ) => \"Flatten to Target Height.\",\r\n\t\t\tnameof( SubNoiseTool ) => \"Noise bump. Ctrl inverts.\",\r\n\t\t\tnameof( SubHoleTool ) => \"Punch hole. Ctrl fills.\",\r\n\t\t\tnameof( SubBridgeTool ) => \"Bridge: press start, release on end.\",\r\n\t\t\t_ => \"Raise height. Ctrl lowers.\"\r\n\t\t};\r\n\t}\r\n\r\n\tstring IntentTooltipForSculpt()\r\n\t{\r\n\t\tvar tip = _brushMode switch\r\n\t\t{\r\n\t\t\tnameof( SubPathTool ) => \"Height-only strip. Control-map Path is Paint Texture (Ctrl+3).\",\r\n\t\t\tnameof( SubSetHeightTool ) => \"Alt-click loads Z into Target Height.\",\r\n\t\t\tnameof( SubPlainerTool ) => \"Like Hammer displacement smoothing across a wide brush.\",\r\n\t\t\tnameof( SubBridgeTool ) => \"Deck sits on the line between the two picked heights. Shortcut B.\",\r\n\t\t\t_ => null\r\n\t\t};\r\n\r\n\t\tvar keys = \"Left / Right or Ctrl+[ / ] brush size. N activates SubTerrain. Keys 1-9, 0 Plainer, B Bridge.\";\r\n\t\treturn tip is null ? keys : $\"{tip} {keys}\";\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Scene view destroys sidebar children on every subtool switch. Unparent first\r\n\t/// from subtool OnDisabled so we can reuse one panel and avoid width/content flash.\r\n\t/// </summary>\r\n\tpublic void DetachSidebarForReuse()\r\n\t{\r\n\t\tif ( _sidebar is not null && _sidebar.IsValid() )\r\n\t\t\t_sidebar.Parent = null;\r\n\t}\r\n\r\n\tpublic void NudgeBrushSize( int direction )\r\n\t{\r\n\t\tBrushSettings.Size = SubTerrainBrushSize.Nudge( BrushSettings.Size, direction );\r\n\t\tSyncBrushSizeWidget();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Sculpt modes only. Paint Texture keeps Up / Down for tmat selection.\r\n\t/// </summary>\r\n\tpublic bool NudgeBrushOpacity( int direction )\r\n\t{\r\n\t\tif ( PaintArmed || direction == 0 )\r\n\t\t\treturn false;\r\n\r\n\t\tconst float step = 0.05f;\r\n\t\tBrushSettings.Opacity = Math.Clamp( BrushSettings.Opacity + direction * step, 0f, 1f );\r\n\t\tRebuildBrushSheet();\r\n\t\treturn true;\r\n\t}\r\n\r\n\tpublic void SyncBrushSizeWidget()\r\n\t{\r\n\t\t_sizeWidget?.SyncFromSettings();\r\n\t}\r\n\r\n\tpublic void SyncBrushRotationRows()\r\n\t{\r\n\t\tif ( _rotationWidget is not null )\r\n\t\t\t_rotationWidget.Enabled = !BrushSettings.RandomRotation;\r\n\t}\r\n\r\n\tpublic bool TrySetPaintLayer( SubTerrainPaintLayer layer )\r\n\t{\r\n\t\tif ( !PaintArmed || _blendWidget is null )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( layer == SubTerrainPaintLayer.Base )\r\n\t\t\t_blendWidget.SelectGround();\r\n\t\telse if ( layer == SubTerrainPaintLayer.Overlay )\r\n\t\t\t_blendWidget.SelectWear();\r\n\t\telse\r\n\t\t\t_blendWidget.SelectPath();\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Cycle selected paint tmat. Path skips the locked ground row (that role is Alt-click).\r\n\t/// </summary>\r\n\tpublic bool NudgePaintMaterial( int delta )\r\n\t{\r\n\t\tif ( !PaintArmed || delta == 0 )\r\n\t\t\treturn false;\r\n\r\n\t\tvar terrain = PeekTerrain();\r\n\t\tvar materials = terrain?.Storage?.Materials;\r\n\t\tint count = materials?.Count ?? 0;\r\n\t\tif ( count <= 0 )\r\n\t\t\treturn false;\r\n\r\n\t\tint skip = -1;\r\n\t\tif ( PaintSettings.Layer == SubTerrainPaintLayer.Path )\r\n\t\t\tskip = SubTerrainSplatUtil.FindPathGround( terrain, PaintSettings );\r\n\r\n\t\tint current = PaintSettings.MaterialIndex;\r\n\t\tif ( current < 0 || current >= count )\r\n\t\t\tcurrent = 0;\r\n\r\n\t\tfor ( int step = 0; step < count; step++ )\r\n\t\t{\r\n\t\t\tcurrent = (current + delta) % count;\r\n\t\t\tif ( current < 0 )\r\n\t\t\t\tcurrent += count;\r\n\r\n\t\t\tif ( current == skip )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tPaintSettings.MaterialIndex = current;\r\n\t\t\tSubTerrainPaintTool.TrySetSplatChannel( current );\r\n\t\t\t_blendWidget?.OnMaterialSelectionChanged();\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tpublic bool StatusValid { get; private set; }\r\n\tpublic Terrain StatusTerrain { get; private set; }\r\n\tpublic Vector3 StatusHitLocal { get; private set; }\r\n\r\n\tpublic static BrushList BrushList => TerrainEditorTool.BrushList;\r\n\r\n\tpublic Texture SelectedBrushTexture => BrushList?.Selected?.Texture;\r\n\r\n\tpublic void UpdateStatus( Terrain terrain, Vector3 hitLocal )\r\n\t{\r\n\t\tStatusValid = true;\r\n\t\tStatusTerrain = terrain;\r\n\t\tStatusHitLocal = hitLocal;\r\n\t}\r\n\r\n\tpublic void ClearStatus()\r\n\t{\r\n\t\tStatusValid = false;\r\n\t\tStatusTerrain = default;\r\n\t}\r\n\r\n\tpublic override IEnumerable<EditorTool> GetSubtools()\r\n\t{\r\n\t\tyield return new SubRaiseLowerTool( this );\r\n\t\tyield return new SubSmoothTool( this );\r\n\t\tyield return new SubPlainerTool( this );\r\n\t\tyield return new SubFlattenTool( this );\r\n\t\tyield return new SubTerrainPaintTool( this );\r\n\t\tyield return new SubSlopeTool( this );\r\n\t\tyield return new SubPathTool( this );\r\n\t\tyield return new SubSetHeightTool( this );\r\n\t\tyield return new SubNoiseTool( this );\r\n\t\tyield return new SubHoleTool( this );\r\n\t\tyield return new SubBridgeTool( this );\r\n\t}\r\n\r\n\tpublic override void OnEnabled()\r\n\t{\r\n\t\tActiveInstance = this;\r\n\t\tAllowGameObjectSelection = false;\r\n\t\tRebuildSidebarOnSelectionChange = false;\r\n\r\n\t\tvar selectedTerrain = GetSelectedComponent<Terrain>();\r\n\t\tif ( !selectedTerrain.IsValid() )\r\n\t\t{\r\n\t\t\tSelection.Clear();\r\n\t\t\tvar first = Scene.GetAllComponents<Terrain>().FirstOrDefault();\r\n\t\t\tif ( first.IsValid() ) Selection.Add( first.GameObject );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override void OnDisabled()\r\n\t{\r\n\t\tif ( ActiveInstance == this )\r\n\t\t\tActiveInstance = null;\r\n\r\n\t\tif ( _sidebar is not null && _sidebar.IsValid() )\r\n\t\t{\r\n\t\t\t_sidebar.Parent = null;\r\n\t\t\t_sidebar.Destroy();\r\n\t\t}\r\n\r\n\t\t_sidebar = null;\r\n\t\t_blendWidget = null;\r\n\t\t_intentLabel = null;\r\n\t\t_brushSheetHost = null;\r\n\t\t_sizeWidget = null;\r\n\t\t_opacityWidget = null;\r\n\t\t_rotationWidget = null;\r\n\t\t_textureSection = null;\r\n\t\t_statusSection = null;\r\n\t\t_statusWidget = null;\r\n\r\n\t\t_previewObject?.Delete();\r\n\t\t_previewObject = null;\r\n\t\tClearStatus();\r\n\t}\r\n\r\n\tpublic override Widget CreateShortcutsWidget() => new SubTerrainToolShortcutsWidget();\r\n\r\n\t[Shortcut( \"tools.subterrain\", \"n\", typeof( SceneViewWidget ) )]\r\n\tpublic static void ActivateTool()\r\n\t{\r\n\t\tEditorToolManager.SetTool( nameof( SubTerrainTool ) );\r\n\t}\r\n\r\n\tpublic void DrawBrushPreview( Transform transform, Terrain terrain = null )\r\n\t{\r\n\t\tvar brush = BrushList?.Selected?.Texture;\r\n\t\tif ( brush is null )\r\n\t\t\treturn;\r\n\r\n\t\t_previewObject ??= new BrushPreviewSceneObject( Gizmo.World );\r\n\r\n\t\tvar color = Color.FromBytes( 150, 150, 250 );\r\n\t\tvar mods = Application.KeyboardModifiers;\r\n\t\tif ( mods.HasFlag( KeyboardModifiers.Alt ) )\r\n\t\t\tcolor = Color.FromBytes( 220, 200, 80 );\r\n\t\telse if ( mods.HasFlag( KeyboardModifiers.Shift ) )\r\n\t\t\tcolor = Color.FromBytes( 120, 200, 140 );\r\n\t\telse if ( mods.HasFlag( KeyboardModifiers.Ctrl ) )\r\n\t\t\tcolor = color.AdjustHue( 90 );\r\n\r\n\t\tcolor.a = BrushSettings.Opacity;\r\n\r\n\t\t_previewObject.RenderLayer = SceneRenderLayer.OverlayWithDepth;\r\n\t\t_previewObject.Bounds = BBox.FromPositionAndSize( 0, float.MaxValue );\r\n\t\t_previewObject.Transform = transform;\r\n\t\t_previewObject.Radius = BrushSettings.Size;\r\n\t\t_previewObject.Texture = brush;\r\n\t\t_previewObject.Color = color;\r\n\t\t_previewObject.BrushRotation = BrushSettings.Rotation;\r\n\r\n\t\tterrain ??= PeekTerrain();\r\n\t\tif ( BrushSettings.ShowGridPreview && terrain?.Storage is not null )\r\n\t\t{\r\n\t\t\tvar tx = terrain.WorldTransform;\r\n\t\t\t_previewObject.CellSize = terrain.Storage.TerrainSize / terrain.Storage.Resolution;\r\n\t\t\t_previewObject.TerrainOrigin = tx.Position;\r\n\t\t\t_previewObject.TerrainRight = tx.Rotation.Right;\r\n\t\t\t_previewObject.TerrainForward = tx.Rotation.Forward;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_previewObject.CellSize = 0f;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override Widget CreateToolSidebar()\r\n\t{\r\n\t\t// Sidebar reuse: detach in OnDisabled before the scene view Clears the host.\r\n\t\tif ( _sidebar is ToolSidebarWidget existing && existing.IsValid() )\r\n\t\t{\r\n\t\t\texisting.Parent = null;\r\n\t\t\tRebuildBrushSheet();\r\n\t\t\tRefreshPaintChrome();\r\n\t\t\treturn existing;\r\n\t\t}\r\n\r\n\t\tvar sidebar = new ToolSidebarWidget();\r\n\t\t_sidebar = sidebar;\r\n\t\tsidebar.AddTitle( \"SubTerrain\", \"terrain\" );\r\n\t\tsidebar.ToolTip = $\"SubTerrain {SubTerrainPaintSettings.LibraryVersion}\";\r\n\r\n\t\t_intentLabel = sidebar.Layout.Add( new Label( \"\" ) );\r\n\t\t_intentLabel.Color = Theme.TextControl;\r\n\t\tsidebar.Layout.AddSpacingCell( 6 );\r\n\r\n\t\t_textureSection = new Widget( sidebar );\r\n\t\t_textureSection.Layout = Layout.Column();\r\n\t\t_blendWidget = new SubTerrainBlendWidget( this, _textureSection );\r\n\t\t_textureSection.Layout.Add( _blendWidget );\r\n\t\tsidebar.Layout.Add( _textureSection );\r\n\t\t_textureSection.Visible = false;\r\n\r\n\t\t{\r\n\t\t\tvar group = sidebar.AddGroup( \"Brush Type\" );\r\n\t\t\tgroup.Add( new SubTerrainBrushPreviewWidget( sidebar ) );\r\n\t\t}\r\n\r\n\t\t{\r\n\t\t\tvar group = sidebar.AddGroup( \"Brush Properties\" );\r\n\t\t\t_brushSheetHost = new Widget( sidebar );\r\n\t\t\t_brushSheetHost.Layout = Layout.Column();\r\n\t\t\tgroup.Add( _brushSheetHost );\r\n\t\t\tRebuildBrushSheet();\r\n\t\t}\r\n\r\n\t\tUpdateOpacityState();\r\n\r\n\t\tif ( !GetSelectedComponent<Terrain>().IsValid() )\r\n\t\t{\r\n\t\t\tvar group = sidebar.AddGroup( \"No Terrain\" );\r\n\t\t\tgroup.Add( new Label( \"Select a GameObject with a Terrain component.\" ) );\r\n\t\t}\r\n\r\n\t\t{\r\n\t\t\tvar cursorLayout = sidebar.AddGroup( \"Cursor\" );\r\n\t\t\t_statusWidget = new SubTerrainStatusWidget( this, sidebar );\r\n\t\t\tcursorLayout.Add( _statusWidget );\r\n\t\t\t_statusSection = FindSidebarGroupHost( _statusWidget );\r\n\t\t}\r\n\r\n\t\tsidebar.AddShortcuts(\r\n\t\t\t(\"Activate SubTerrain\", \"N\"),\r\n\t\t\t(\"Sculpt / paint tools\", \"1 - 9\"),\r\n\t\t\t(\"Plainer / Bridge\", \"0 / B\"),\r\n\t\t\t(\"Paint: Ground / Wear / Path\", \"Ctrl+1 / 2 / 3\"),\r\n\t\t\t(\"Paint tmat prev / next\", \"Up / Down\"),\r\n\t\t\t(\"Opacity (sculpt)\", \"Up / Down\"),\r\n\t\t\t(\"Brush size\", \"Left / Right | Ctrl+[ / ]\"),\r\n\t\t\t(\"Size / opacity\", \"MMB+Shift drag\"),\r\n\t\t\t(\"Brush rotation\", \"MMB+Ctrl drag\"),\r\n\t\t\t(\"Sample / fade / smooth\", \"Alt / Ctrl / Shift\")\r\n\t\t);\r\n\r\n\t\tsidebar.Layout.AddStretchCell();\r\n\t\tUpdateIntent();\r\n\t\treturn sidebar;\r\n\t}\r\n\r\n\tstatic Widget FindSidebarGroupHost( Widget child )\r\n\t{\r\n\t\tfor ( var p = child?.Parent; p is not null; p = p.Parent )\r\n\t\t{\r\n\t\t\tif ( p.GetType().Name == \"SidebarGroupWidget\" )\r\n\t\t\t\treturn p;\r\n\t\t}\r\n\r\n\t\treturn child?.Parent;\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void TickSidebarChrome()\r\n\t{\r\n\t\tif ( ActiveInstance != this )\r\n\t\t\treturn;\r\n\r\n\t\tif ( !PaintArmed )\r\n\t\t\tUpdateIntent();\r\n\t}\r\n\r\n\tvoid RebuildBrushSheet()\r\n\t{\r\n\t\tif ( _brushSheetHost is null )\r\n\t\t\treturn;\r\n\r\n\t\t_brushSheetHost.Layout.Clear( true );\r\n\t\t_sizeWidget = null;\r\n\t\t_opacityWidget = null;\r\n\t\t_speedWidget = null;\r\n\t\t_rotationWidget = null;\r\n\r\n\t\t_sizeWidget = new SubTerrainBrushSizeWidget( this, _brushSheetHost );\r\n\t\t_brushSheetHost.Layout.Add( _sizeWidget );\r\n\r\n\t\tvar so = BrushSettings.GetSerialized();\r\n\t\tvar sheet = new ControlSheet();\r\n\t\t_opacityWidget = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.Opacity ) ) );\r\n\t\tif ( _brushMode == nameof( SubRaiseLowerTool ) )\r\n\t\t\t_speedWidget = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.Speed ) ) );\r\n\t\t_rotationWidget = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.Rotation ) ) );\r\n\t\tvar rndRot = sheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.RandomRotation ) ) );\r\n\t\tsheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.ShowGridPreview ) ) );\r\n\t\tif ( _brushMode == nameof( SubSetHeightTool ) )\r\n\t\t\tsheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.TargetHeight ) ) );\r\n\t\tif ( _brushMode == nameof( SubBridgeTool ) )\r\n\t\t{\r\n\t\t\tsheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.BridgeWidth ) ) );\r\n\t\t\tsheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.BridgeEdgeFalloff ) ) );\r\n\t\t\tsheet.AddRow( so.GetProperty( nameof( SubTerrainBrushSettings.BridgeRaiseOnly ) ) );\r\n\t\t}\r\n\r\n\t\tso.OnPropertyChanged += OnBrushSheetPropertyChanged;\r\n\r\n\t\t_brushSheetHost.Layout.Add( sheet );\r\n\t\tSyncBrushRotationRows();\r\n\t\tUpdateOpacityState();\r\n\t}\r\n\r\n\tvoid OnBrushSheetPropertyChanged( SerializedProperty prop )\r\n\t{\r\n\t\tif ( prop?.Name == nameof( SubTerrainBrushSettings.RandomRotation ) )\r\n\t\t\tSyncBrushRotationRows();\r\n\t}\r\n\r\n\tvoid UpdateOpacityState()\r\n\t{\r\n\t\tif ( _speedWidget is not null )\r\n\t\t\t_speedWidget.ToolTip = \"How fast height accumulates. Hold still to keep raising. Higher = more stamps and more height per stamp.\";\r\n\r\n\t\tif ( _opacityWidget is null )\r\n\t\t\treturn;\r\n\r\n\t\tif ( !PaintArmed )\r\n\t\t{\r\n\t\t\tbool bridge = _brushMode == nameof( SubBridgeTool );\r\n\t\t\t_opacityWidget.Enabled = !bridge;\r\n\t\t\t_opacityWidget.ToolTip = bridge\r\n\t\t\t\t? \"Bridge always builds at the exact start and end heights.\"\r\n\t\t\t\t: OpacityTipForSculpt();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar layer = PaintSettings.Layer;\r\n\t\tif ( layer == SubTerrainPaintLayer.Overlay )\r\n\t\t{\r\n\t\t\t_opacityWidget.Enabled = true;\r\n\t\t\t_opacityWidget.ToolTip = \"How fast wear builds on this stroke. Ctrl reduces.\";\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_opacityWidget.Enabled = false;\r\n\t\t_opacityWidget.ToolTip = layer == SubTerrainPaintLayer.Path\r\n\t\t\t? \"Path uses Center mix and Core for the apron / solid ribbon. Pack center optional. Soft circle or stroke.\"\r\n\t\t\t: \"Ground paint lays a solid tmat. Mix is cleared.\";\r\n\t}\r\n\r\n\tstring OpacityTipForSculpt()\r\n\t{\r\n\t\treturn _brushMode switch\r\n\t\t{\r\n\t\t\tnameof( SubRaiseLowerTool ) => \"Brush soft strength. Ctrl lowers. Use Speed for how fast height builds.\",\r\n\t\t\tnameof( SubHoleTool ) => \"How fast the punch cuts. Ctrl fills the hole back.\",\r\n\t\t\tnameof( SubNoiseTool ) => \"How strong the bump is. Ctrl inverts.\",\r\n\t\t\tnameof( SubFlattenTool ) => \"How fast it levels to the click height.\",\r\n\t\t\tnameof( SubSetHeightTool ) => \"How fast it levels to Target Height.\",\r\n\t\t\tnameof( SubSlopeTool ) => \"How fast it grades to the ramp.\",\r\n\t\t\tnameof( SubPathTool ) => \"How fast it levels to the click height.\",\r\n\t\t\tnameof( SubSmoothTool ) => \"How strong the smooth is.\",\r\n\t\t\tnameof( SubPlainerTool ) => \"How strong the weighted leveling is.\",\r\n\t\t\t_ => \"Stroke strength.\"\r\n\t\t};\r\n\t}\r\n}\r\n\r\n[Title( \"Raise / Lower\" )]\r\n[Description( \"Paint height up. Hold Ctrl to lower.\" )]\r\n[Icon( \"height\" )]\r\n[Alias( \"subterrain_raise_lower\" )]\r\n[Group( \"1\" )]\r\n[Order( 0 )]\r\npublic class SubRaiseLowerTool : SubTerrainBaseBrushTool\r\n{\r\n\tpublic SubRaiseLowerTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.RaiseLower;\r\n\t\tAllowBrushInvert = true;\r\n\t}\r\n}\r\n\r\n[Title( \"Flatten\" )]\r\n[Description( \"Level to the height under the click. Stays on that plane while you drag.\" )]\r\n[Icon( \"trending_flat\" )]\r\n[Alias( \"subterrain_flatten\" )]\r\n[Group( \"1\" )]\r\n[Order( 3 )]\r\npublic class SubFlattenTool : SubTerrainBaseBrushTool\r\n{\r\n\tpublic SubFlattenTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.Flatten;\r\n\t}\r\n\r\n\tpublic override bool GetHitPosition( Terrain terrain, out Vector3 position )\r\n\t{\r\n\t\tif ( _dragging )\r\n\t\t{\r\n\t\t\tvar tx = terrain.WorldTransform;\r\n\t\t\tvar hit = StrokePlane.Trace( Gizmo.CurrentRay, true );\r\n\t\t\tposition = tx.PointToLocal( hit.Value );\r\n\t\t\treturn hit.HasValue;\r\n\t\t}\r\n\r\n\t\treturn base.GetHitPosition( terrain, out position );\r\n\t}\r\n}\r\n\r\n[Title( \"Smooth\" )]\r\n[Description( \"Blend neighbouring heights.\" )]\r\n[Icon( \"rounded_corner\" )]\r\n[Alias( \"subterrain_smooth\" )]\r\n[Group( \"1\" )]\r\n[Order( 1 )]\r\npublic class SubSmoothTool : SubTerrainBaseBrushTool\r\n{\r\n\tpublic SubSmoothTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.Smooth;\r\n\t}\r\n\r\n\tpublic override bool GetHitPosition( Terrain terrain, out Vector3 position )\r\n\t{\r\n\t\tif ( _dragging )\r\n\t\t{\r\n\t\t\tvar tx = terrain.WorldTransform;\r\n\t\t\tvar hit = StrokePlane.Trace( Gizmo.CurrentRay, true );\r\n\t\t\tposition = tx.PointToLocal( hit.Value );\r\n\t\t\treturn hit.HasValue;\r\n\t\t}\r\n\r\n\t\treturn base.GetHitPosition( terrain, out position );\r\n\t}\r\n}\r\n\r\n[Title( \"Plainer\" )]\r\n[Description( \"Gradually level a broad weighted area, like Hammer displacement smoothing.\" )]\r\n[Icon( \"blur_on\" )]\r\n[Alias( \"subterrain_plainer\" )]\r\n[Group( \"1\" )]\r\n[Order( 2 )]\r\npublic class SubPlainerTool : SubTerrainBaseBrushTool\r\n{\r\n\tpublic SubPlainerTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.Plainer;\r\n\t}\r\n\r\n\tpublic override bool GetHitPosition( Terrain terrain, out Vector3 position )\r\n\t{\r\n\t\tif ( _dragging )\r\n\t\t{\r\n\t\t\tvar tx = terrain.WorldTransform;\r\n\t\t\tvar hit = StrokePlane.Trace( Gizmo.CurrentRay, true );\r\n\t\t\tposition = tx.PointToLocal( hit.Value );\r\n\t\t\treturn hit.HasValue;\r\n\t\t}\r\n\r\n\t\treturn base.GetHitPosition( terrain, out position );\r\n\t}\r\n}\r\n\r\n[Title( \"Noise\" )]\r\n[Description( \"Add bump. Hold Ctrl to invert.\" )]\r\n[Icon( \"grain\" )]\r\n[Alias( \"subterrain_noise\" )]\r\n[Group( \"1\" )]\r\n[Order( 8 )]\r\npublic class SubNoiseTool : SubTerrainBaseBrushTool\r\n{\r\n\tpublic SubNoiseTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.Noise;\r\n\t\tAllowBrushInvert = true;\r\n\t}\r\n}\r\n\r\n[Title( \"Hole\" )]\r\n[Description( \"Punch through the surface. Hold Ctrl to fill the hole back.\" )]\r\n[Icon( \"trip_origin\" )]\r\n[Alias( \"subterrain_hole\" )]\r\n[Group( \"1\" )]\r\n[Order( 9 )]\r\npublic class SubHoleTool : SubTerrainBaseBrushTool\r\n{\r\n\tpublic SubHoleTool( SubTerrainTool parent ) : base( parent )\r\n\t{\r\n\t\tMode = SubTerrainSculptMode.Hole;\r\n\t\tAllowBrushInvert = true;\r\n\t}\r\n}\r\n\r\ninternal class SubTerrainBrushPreviewWidget : Widget\r\n{\r\n\tpublic SubTerrainBrushPreviewWidget( Widget parent ) : base( parent )\r\n\t{\r\n\t\tMinimumSize = new( 32, 32 );\r\n\t\tMaximumSize = new( 48, 48 );\r\n\t\tCursor = CursorShape.Finger;\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.DrawRect( LocalRect );\r\n\r\n\t\tvar pixmap = SubTerrainTool.BrushList?.Selected?.Pixmap;\r\n\t\tif ( pixmap != null )\r\n\t\t{\r\n\t\t\tPaint.Draw( LocalRect.Contain( pixmap.Size ), pixmap );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnMouseClick( MouseEvent e )\r\n\t{\r\n\t\tvar popup = new PopupWidget( null );\r\n\t\tpopup.Position = Application.CursorPosition;\r\n\t\tpopup.Visible = true;\r\n\t\tpopup.Layout = Layout.Column();\r\n\t\tpopup.Layout.Margin = 10;\r\n\t\tpopup.MaximumSize = new Vector2( 300, 200 );\r\n\r\n\t\tvar list = new SubTerrainBrushListWidget();\r\n\t\tlist.BrushSelected += () =>\r\n\t\t{\r\n\t\t\tpopup.Close();\r\n\t\t\tUpdate();\r\n\t\t};\r\n\t\tpopup.Layout.Add( list );\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void UpdatePreview()\r\n\t{\r\n\t\tUpdate();\r\n\t}\r\n}\r\n\r\ninternal class SubTerrainBrushListWidget : Widget\r\n{\r\n\tpublic Action BrushSelected { get; set; }\r\n\r\n\tpublic SubTerrainBrushListWidget() : base( null )\r\n\t{\r\n\t\tListView list = new();\r\n\t\tlist.SetItems( SubTerrainTool.BrushList.Brushes.Cast<object>() );\r\n\t\tlist.ItemSize = new Vector2( 40, 40 );\r\n\t\tlist.ItemAlign = Sandbox.UI.Align.SpaceBetween;\r\n\t\tlist.OnPaintOverride += () => PaintListBackground( list );\r\n\t\tlist.ItemPaint = PaintBrushItem;\r\n\t\tlist.ItemSelected = item =>\r\n\t\t{\r\n\t\t\tif ( item is Brush brush )\r\n\t\t\t{\r\n\t\t\t\tSubTerrainTool.BrushList.Selected = brush;\r\n\t\t\t\tBrushSelected?.Invoke();\r\n\t\t\t}\r\n\t\t};\r\n\t\tlist.SelectItem( SubTerrainTool.BrushList.Selected );\r\n\r\n\t\tLayout = Layout.Column();\r\n\r\n\t\tvar label = new Label( \"Brushes\" );\r\n\t\tlabel.SetStyles( \"font-weight: bold\" );\r\n\t\tLayout.Add( label );\r\n\t\tLayout.AddSpacingCell( 8 );\r\n\t\tLayout.Add( list );\r\n\t}\r\n\r\n\tprivate void PaintBrushItem( VirtualWidget widget )\r\n\t{\r\n\t\tvar brush = (Brush)widget.Object;\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.TextAntialiasing = true;\r\n\r\n\t\tif ( widget.Hovered || widget.Selected )\r\n\t\t{\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tPaint.SetBrush( widget.Selected ? Theme.Primary : Color.White.WithAlpha( 0.1f ) );\r\n\t\t\tPaint.DrawRect( widget.Rect.Grow( 2 ), 3 );\r\n\t\t}\r\n\r\n\t\tPaint.Draw( widget.Rect, brush.Pixmap );\r\n\t}\r\n\r\n\tprivate bool PaintListBackground( Widget widget )\r\n\t{\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( Theme.ControlBackground );\r\n\t\tPaint.DrawRect( widget.LocalRect );\r\n\t\treturn false;\r\n\t}\r\n}\r\n\r\ninternal class SubTerrainToolShortcutsWidget : Widget\r\n{\r\n\t[Shortcut( \"tools.subterrain.raise-lower\", \"1\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateRaiseLower() => EditorToolManager.SetSubTool( nameof( SubRaiseLowerTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.smooth\", \"2\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateSmooth() => EditorToolManager.SetSubTool( nameof( SubSmoothTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.flatten\", \"3\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateFlatten() => EditorToolManager.SetSubTool( nameof( SubFlattenTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.paint\", \"4\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivatePaint() => EditorToolManager.SetSubTool( nameof( SubTerrainPaintTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.slope\", \"5\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateSlope() => EditorToolManager.SetSubTool( nameof( SubSlopeTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.path\", \"6\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivatePath() => EditorToolManager.SetSubTool( nameof( SubPathTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.set-height\", \"7\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateSetHeight() => EditorToolManager.SetSubTool( nameof( SubSetHeightTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.noise\", \"8\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateNoise() => EditorToolManager.SetSubTool( nameof( SubNoiseTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.hole\", \"9\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateHole() => EditorToolManager.SetSubTool( nameof( SubHoleTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.plainer\", \"0\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivatePlainer() => EditorToolManager.SetSubTool( nameof( SubPlainerTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.bridge\", \"B\", typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateBridge() => EditorToolManager.SetSubTool( nameof( SubBridgeTool ) );\r\n\r\n\t[Shortcut( \"tools.subterrain.paint-ground\", \"CTRL+1\", typeof( SceneViewWidget ) )]\r\n\tpublic void PaintGround() => SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Base );\r\n\r\n\t[Shortcut( \"tools.subterrain.paint-wear\", \"CTRL+2\", typeof( SceneViewWidget ) )]\r\n\tpublic void PaintWear() => SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Overlay );\r\n\r\n\t[Shortcut( \"tools.subterrain.paint-path\", \"CTRL+3\", typeof( SceneViewWidget ) )]\r\n\tpublic void PaintPath() => SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Path );\r\n\r\n\t[Shortcut( \"tools.subterrain.brush-smaller\", \"CTRL+[\", typeof( SceneViewWidget ) )]\r\n\tpublic void BrushSmaller() => SubTerrainTool.ActiveInstance?.NudgeBrushSize( -1 );\r\n\r\n\t[Shortcut( \"tools.subterrain.brush-larger\", \"CTRL+]\", typeof( SceneViewWidget ) )]\r\n\tpublic void BrushLarger() => SubTerrainTool.ActiveInstance?.NudgeBrushSize( 1 );\r\n\r\n\t[Shortcut( \"tools.subterrain.brush-smaller-arrow\", \"LEFT\", typeof( SceneViewWidget ) )]\r\n\tpublic void BrushSmallerArrow()\r\n\t{\r\n\t\tif ( SubTerrainTool.ActiveInstance is not null )\r\n\t\t{\r\n\t\t\tSubTerrainTool.ActiveInstance.NudgeBrushSize( -1 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEditorShortcuts.PassShortcut = true;\r\n\t}\r\n\r\n\t[Shortcut( \"tools.subterrain.brush-larger-arrow\", \"RIGHT\", typeof( SceneViewWidget ) )]\r\n\tpublic void BrushLargerArrow()\r\n\t{\r\n\t\tif ( SubTerrainTool.ActiveInstance is not null )\r\n\t\t{\r\n\t\t\tSubTerrainTool.ActiveInstance.NudgeBrushSize( 1 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEditorShortcuts.PassShortcut = true;\r\n\t}\r\n\r\n\t[Shortcut( \"tools.subterrain.paint-tmat-prev\", \"UP\", typeof( SceneViewWidget ) )]\r\n\tpublic void ArrowUp()\r\n\t{\r\n\t\tvar tool = SubTerrainTool.ActiveInstance;\r\n\t\tif ( tool is null )\r\n\t\t{\r\n\t\t\tEditorShortcuts.PassShortcut = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( tool.PaintArmed )\r\n\t\t{\r\n\t\t\tif ( tool.NudgePaintMaterial( -1 ) )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tEditorShortcuts.PassShortcut = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\ttool.NudgeBrushOpacity( 1 );\r\n\t}\r\n\r\n\t[Shortcut( \"tools.subterrain.paint-tmat-next\", \"DOWN\", typeof( SceneViewWidget ) )]\r\n\tpublic void ArrowDown()\r\n\t{\r\n\t\tvar tool = SubTerrainTool.ActiveInstance;\r\n\t\tif ( tool is null )\r\n\t\t{\r\n\t\t\tEditorShortcuts.PassShortcut = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( tool.PaintArmed )\r\n\t\t{\r\n\t\t\tif ( tool.NudgePaintMaterial( 1 ) )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tEditorShortcuts.PassShortcut = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\ttool.NudgeBrushOpacity( -1 );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "subzerostudios.subterrain-tool",
            "Path": "Editor/SubTerrainBrushSizeWidget.cs",
            "FileName": "SubTerrainBrushSizeWidget.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 344638,
            "Code": "// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.\r\n\r\nusing System;\r\nusing Sandbox;\r\n\r\nnamespace Editor.SubTerrain;\r\n\r\n/// <summary>\r\n/// Brush Size row: piecewise track (fine under 3500, mid to 18000, coarse to 35000) plus type-in.\r\n/// </summary>\r\ninternal sealed class SubTerrainBrushSizeWidget : Widget\r\n{\r\n\treadonly SubTerrainTool _tool;\r\n\treadonly SubTerrainBrushSizeTrack _track;\r\n\treadonly LineEdit _field;\r\n\tbool _syncing;\r\n\r\n\tpublic SubTerrainBrushSizeWidget( SubTerrainTool tool, Widget parent ) : base( parent )\r\n\t{\r\n\t\t_tool = tool;\r\n\r\n\t\tLayout = Layout.Row();\r\n\t\tLayout.Spacing = 6;\r\n\t\tLayout.Margin = new Sandbox.UI.Margin( 0, 2, 10, 2 );\r\n\r\n\t\tvar label = Layout.Add( new Label( \"Size\" ) );\r\n\t\tlabel.MinimumWidth = 72;\r\n\t\tlabel.Color = Theme.TextControl;\r\n\t\tlabel.ToolTip = \"World units. Fine under 3500 (step 50), mid to 18000 (step 500), then to 35000 (step 1000). Left / Right or Ctrl+[ / ] steps.\";\r\n\r\n\t\t_track = new SubTerrainBrushSizeTrack( this );\r\n\t\tLayout.Add( _track, 1 );\r\n\r\n\t\t_field = new LineEdit();\r\n\t\t_field.MinimumWidth = 52;\r\n\t\t_field.MaximumWidth = 56;\r\n\t\t_field.ToolTip = \"Type an exact size (8-35000).\";\r\n\t\t_field.EditingFinished += OnFieldFinished;\r\n\t\tLayout.Add( _field );\r\n\r\n\t\tFixedHeight = 28;\r\n\t\tMinimumHeight = 28;\r\n\t\tMaximumHeight = 28;\r\n\r\n\t\tSyncFromSettings();\r\n\t}\r\n\r\n\tpublic void SyncFromSettings()\r\n\t{\r\n\t\tif ( _tool?.BrushSettings is null )\r\n\t\t\treturn;\r\n\r\n\t\t_syncing = true;\r\n\t\tint size = SubTerrainBrushSize.Clamp( _tool.BrushSettings.Size );\r\n\t\t_tool.BrushSettings.Size = size;\r\n\t\t_field.Text = size.ToString();\r\n\t\t_track.Update();\r\n\t\t_syncing = false;\r\n\t}\r\n\r\n\tpublic void SetSizeFromTrack( float t01 )\r\n\t{\r\n\t\tif ( _syncing || _tool?.BrushSettings is null )\r\n\t\t\treturn;\r\n\r\n\t\tint size = SubTerrainBrushSize.SizeFromTrack( t01 );\r\n\t\tif ( size == _tool.BrushSettings.Size )\r\n\t\t{\r\n\t\t\t_track.Update();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_tool.BrushSettings.Size = size;\r\n\t\t_syncing = true;\r\n\t\t_field.Text = size.ToString();\r\n\t\t_syncing = false;\r\n\t\t_track.Update();\r\n\t}\r\n\r\n\tvoid OnFieldFinished()\r\n\t{\r\n\t\tif ( _syncing || _tool?.BrushSettings is null )\r\n\t\t\treturn;\r\n\r\n\t\tif ( !int.TryParse( _field.Text.Trim(), out int parsed ) )\r\n\t\t{\r\n\t\t\tSyncFromSettings();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_tool.BrushSettings.Size = SubTerrainBrushSize.Clamp( parsed );\r\n\t\tSyncFromSettings();\r\n\t}\r\n}\r\n\r\ninternal sealed class SubTerrainBrushSizeTrack : Widget\r\n{\r\n\treadonly SubTerrainBrushSizeWidget _host;\r\n\tbool _dragging;\r\n\r\n\tpublic SubTerrainBrushSizeTrack( SubTerrainBrushSizeWidget host ) : base( host )\r\n\t{\r\n\t\t_host = host;\r\n\t\tMouseTracking = true;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tMinimumHeight = 22;\r\n\t\tMaximumHeight = 22;\r\n\t\tToolTip = \"Drag: fine under 3500, mid to 18000, coarse to 35000. Left / Right or Ctrl+[ / ] use the same steps.\";\r\n\t}\r\n\r\n\tprotected override void OnMousePress( MouseEvent e )\r\n\t{\r\n\t\tif ( e.Button == MouseButtons.Left )\r\n\t\t{\r\n\t\t\t_dragging = true;\r\n\t\t\tApplyAt( e.LocalPosition.x );\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbase.OnMousePress( e );\r\n\t}\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseMove( e );\r\n\t\tif ( _dragging )\r\n\t\t\tApplyAt( e.LocalPosition.x );\r\n\t}\r\n\r\n\tprotected override void OnMouseReleased( MouseEvent e )\r\n\t{\r\n\t\tif ( e.Button == MouseButtons.Left )\r\n\t\t\t_dragging = false;\r\n\r\n\t\tbase.OnMouseReleased( e );\r\n\t}\r\n\r\n\tvoid ApplyAt( float x )\r\n\t{\r\n\t\tfloat w = Math.Max( Width - 8f, 1f );\r\n\t\tfloat t = Math.Clamp( (x - 4f) / w, 0f, 1f );\r\n\t\t_host.SetSizeFromTrack( t );\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tbase.OnPaint();\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\r\n\t\tvar track = new Rect( 4, Height * 0.5f - 2f, Math.Max( Width - 8f, 1f ), 4f );\r\n\t\tPaint.SetBrush( Color.White.WithAlpha( 0.08f ) );\r\n\t\tPaint.DrawRect( track, 2 );\r\n\r\n\t\t// Piecewise band ticks (fine / mid).\r\n\t\tfloat fineX = track.Left + track.Width * SubTerrainBrushSize.FineTrack;\r\n\t\tfloat midX = track.Left + track.Width * SubTerrainBrushSize.MidTrack;\r\n\t\tPaint.SetBrush( Color.White.WithAlpha( 0.14f ) );\r\n\t\tPaint.DrawRect( new Rect( fineX, track.Top - 2f, 1f, track.Height + 4f ) );\r\n\t\tPaint.DrawRect( new Rect( midX, track.Top - 2f, 1f, track.Height + 4f ) );\r\n\r\n\t\tvar tool = SubTerrainTool.ActiveInstance;\r\n\t\tint size = tool?.BrushSettings is not null\r\n\t\t\t? SubTerrainBrushSize.Clamp( tool.BrushSettings.Size )\r\n\t\t\t: SubTerrainBrushSize.Min;\r\n\r\n\t\tfloat t = SubTerrainBrushSize.TrackFromSize( size );\r\n\t\tfloat thumbX = track.Left + track.Width * t;\r\n\t\tfloat thumbR = 6f;\r\n\t\tPaint.SetBrush( Theme.Primary );\r\n\t\tPaint.DrawCircle( new Vector2( thumbX, Height * 0.5f ), thumbR );\r\n\t}\r\n}\r\n"
        }
    ]
}