Editor/SubTerrainTool.cs
// 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 );
}
}