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