{"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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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( \u0022AddonTitle\u0022, \u0022SubTerrain Tool\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022subterrain-tool\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022subzerostudios\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022subzerostudios.subterrain-tool\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002228\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-08-24T22:24:58.1339886Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.165.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.165.0\u0022)]"},{"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/// \u003Csummary\u003E0 = kind auto-scale (radius \u00D7 DepthScale, clamped).\u003C/summary\u003E\r\n\tpublic float Depth;\r\n\r\n\t/// \u003Csummary\u003EUnused this pass. Reserved for a later trench (capsule SDF).\u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Paint Texture sidebar: tmat list, Ground / Wear / Path, Steep / All, live mix.\r\n/// \u003C/summary\u003E\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 = \u0022 Alt sample | Ctrl fade | Shift smooth | ring yellow/green = Alt/Shift.\u0022;\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( \u0022\u0022 ) );\r\n\r\n\t\tRebuildList();\r\n\t\tRebuildOptions();\r\n\t\tRefreshCopy();\r\n\t}\r\n\r\n\tvoid OnGroundClicked() =\u003E SetLayer( SubTerrainPaintLayer.Base );\r\n\r\n\tvoid OnWearClicked() =\u003E 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() =\u003E OnGroundClicked();\r\n\tinternal void SelectWear() =\u003E OnWearClicked();\r\n\tinternal void SelectPath() =\u003E 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( \u0022This terrain has no materials to paint.\u0022 ) );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfor ( int i = 0; i \u003C count; i\u002B\u002B )\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 \u0026\u0026 _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( \u0022Keep pair\u0022 );\r\n\t\t\t_keepPairBox.Value = _tool.PaintSettings.MixOnly;\r\n\t\t\t_keepPairBox.ToolTip = \u0022Only move mix. Do not change the base/overlay pair (avoids texel stairs next to a different pair).\u0022;\r\n\t\t\t_keepPairBox.Toggled \u002B= OnKeepPairToggled;\r\n\t\t\t_optionsHost.Layout.Add( _keepPairBox );\r\n\r\n\t\t\t_sampledBox = new Checkbox( \u0022Stay on sampled\u0022 );\r\n\t\t\t_sampledBox.Value = _tool.PaintSettings.LimitToSampledGround;\r\n\t\t\t_sampledBox.ToolTip = \u0022Alt-click locks to that ground base. Wear will not paint other bases.\u0022;\r\n\t\t\t_sampledBox.Toggled \u002B= 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( \u0022Pack center\u0022 );\r\n\t\t\t_packCenterBox.Value = _tool.PaintSettings.PackCenter;\r\n\t\t\t_packCenterBox.ToolTip = \u0022On: solid path core along the stroke, soft apron outside. Off: soft ground \u002B path apron only.\u0022;\r\n\t\t\t_packCenterBox.Toggled \u002B= 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 ) \u0026\u0026 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 \u0026\u0026 _tool.PaintSettings.Dithers )\r\n\t\t\tline \u002B= \u0022 | dither\u0022;\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( \u00220.00\u0022 );\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 \u0022Click the packed tmat. Alt-click the ground.\u0022;\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? $\u0022{wName} center | {gName} | {wName} {mix} | core {settings.PackCore:0.00}\u0022\r\n\t\t\t\t: $\u0022{gName} | {wName} {mix} | soft\u0022;\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 = \u0022\u0022;\r\n\t\tif ( settings.LimitToSampledGround \u0026\u0026 settings.HasSampledGround )\r\n\t\t\tlockOn = $\u0022 on {SubTerrainSplatUtil.MaterialName( terrain, settings.SampledGroundIndex )}\u0022;\r\n\r\n\t\tif ( settings.MixOnly )\r\n\t\t\treturn $\u0022mix {name} {mix}{lockOn}\u0022;\r\n\r\n\t\treturn settings.OnlyOnSlope\r\n\t\t\t? $\u0022{name} {mix} steep{lockOn}\u0022\r\n\t\t\t: $\u0022{name} {mix}{lockOn}\u0022;\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 =\u003E settings.PackCenter\r\n\t\t\t\t? \u0022Pack center on. Raise Core for a wider solid middle. Center mix is the soft apron. Brush Size widens the whole blend.\u0022\r\n\t\t\t\t: \u0022Pack center off. Soft apron only: ground \u002B path. Center mix is the apron mix.\u0022,\r\n\t\t\tSubTerrainPaintLayer.Base =\u003E \u0022Ground writes the selected tmat as base and overlay, mix 0.\u0022,\r\n\t\t\t_ =\u003E \u0022Wear writes overlay mix. Steep skips flats. Ctrl reduces. Alt-click locks Stay on sampled.\u0022\r\n\t\t};\r\n\r\n\t\treturn body \u002B 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 = \u0022Alt-click the ground to lock Stay on sampled.\u0022;\r\n\t\t\telse\r\n\t\t\t\t_sampledBox.ToolTip = \u0022Locked to the Alt-clicked ground base. Uncheck to paint any base.\u0022;\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() \u002B \u0022 | hole\u0022;\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 \u003C 0.02f\r\n\t\t\t? baseName\r\n\t\t\t: $\u0022{baseName} \u002B {overlayName} {mix:0.00}\u0022;\r\n\r\n\t\tstring extra = \u0022\u0022;\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 \u0026\u0026 settings.OnlyOnSlope;\r\n\t\t\tif ( steepGate )\r\n\t\t\t\textra = degrees \u003E= settings.SlopeDegrees ? $\u0022 | steep {deg}\u0022 : $\u0022 | skip {deg}\u0022;\r\n\t\t}\r\n\r\n\t\tif ( SubTerrainSplatUtil.HasSeam( terrain, x, y ) )\r\n\t\t\textra \u002B= \u0022 | seam\u0022;\r\n\r\n\t\t_live.Text = $\u0022{WriteLine()} | {there}{extra}\u0022;\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 = \u0022Pair this stroke will write.\u0022;\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 \u002B 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 \u002B ThumbSize \u002B 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 \u003C 0 || index \u003E= 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 = \u0022Ground is a solid tmat. Wear is overlay mix. Path packs a path tmat (optional Pack center) with a ground\u002Bpath apron.\u0022;\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 \u003C SlotCount; i\u002B\u002B )\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 =\u003E layer == SubTerrainPaintLayer.Base,\r\n\t\t\t\t1 =\u003E layer == SubTerrainPaintLayer.Overlay,\r\n\t\t\t\t_ =\u003E 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 \u003E 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 \u002B 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 =\u003E \u0022Ground\u0022,\r\n\t\t\t\t1 =\u003E \u0022Wear\u0022,\r\n\t\t\t\t_ =\u003E \u0022Path\u0022\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 = \u0022Steep skips ground that is too flat. All paints the whole brush, including shelves.\u0022;\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? \u0022Skip ground that is too flat. Hover a hill; the footer says steep or skip.\u0022\r\n\t\t\t: \u0022Paint the whole brush, including shelves.\u0022;\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 = \u0022Steep skips ground that is too flat. All paints the whole brush, including shelves.\u0022;\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 \u003C SlotCount; i\u002B\u002B )\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 \u003E 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 \u002B 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 ? \u0022Steep\u0022 : \u0022All\u0022, 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: \u003E 0 } mats \u0026\u0026 index \u003E= 0 \u0026\u0026 index \u003C 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 \u0026\u0026 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 \u0026\u0026 _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 \u0026\u0026 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 \u0026\u0026 !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 \u002B 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 = \u0022ground\u0022;\r\n\t\telse if ( path \u0026\u0026 selected )\r\n\t\t\trole = \u0022path\u0022;\r\n\t\telse if ( !path \u0026\u0026 selected \u0026\u0026 settings.Layer == SubTerrainPaintLayer.Overlay )\r\n\t\t\trole = settings.MixCap.ToString( \u00220.00\u0022 );\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? \u0022Click the packed tmat. Alt-click the ground under it. Double-click opens the asset.\u0022\r\n\t\t\t: _name \u002B \u0022 | Double-click opens the asset.\u0022;\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n[Title( \u0022Terrain Sculpt Net\u0022 )]\r\n[Category( \u0022SubTerrain\u0022 )]\r\n[Icon( \u0022share\u0022 )]\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( \u0022Depth Scale\u0022 ), 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( \u0022Depth Scale\u0022 ), 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\n[Title( \u0022Bridge\u0022 )]\n[Description( \u0022Press start, release on end. Raises a land bridge between the two heights.\u0022 )]\n[Icon( \u0022linear_scale\u0022 )]\n[Alias( \u0022subterrain_bridge\u0022 )]\n[Group( \u00221\u0022 )]\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 \u003C 1.0f )\n\t\t\treturn;\n\n\t\tint size = (int)Math.Ceiling( Math.Max( Math.Abs( spanTexels.x ), Math.Abs( spanTexels.y ) ) \u002B _halfWidthTexels * 2.0f ) \u002B 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 \u002B _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( \u0022SlopeEndUV\u0022, _endUV );\n\t\tcs.Attributes.Set( \u0022SlopeEndHeight\u0022, paint.FlattenHeight );\n\t\tcs.Attributes.Set( \u0022BridgeHalfWidth\u0022, _halfWidthTexels );\n\t\tcs.Attributes.Set( \u0022BridgeDeckRatio\u0022, 1.0f - settings.BridgeEdgeFalloff );\n\t\tcs.Attributes.Set( \u0022BridgeRaiseOnly\u0022, 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 \u003C 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 \u002B side, worldCenter \u002B side );\n\t\tGizmo.Draw.Line( startWorld - side, worldCenter - side );\n\t\tGizmo.Draw.Line( startWorld - side, startWorld \u002B side );\n\t\tGizmo.Draw.Line( worldCenter - side, worldCenter \u002B 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/// \u003Csummary\u003E\r\n/// Flatten to Target Height. Alt-click the ground to load that Z.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Set Height\u0022 )]\r\n[Icon( \u0022straighten\u0022 )]\r\n[Alias( \u0022subterrain_set_height\u0022 )]\r\n[Group( \u00221\u0022 )]\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/// \u003Csummary\u003E\r\n/// Runtime heightfield editor: queue CPU stamps, then SyncGPUTexture \u002B region UpdateCollision.\r\n/// Play-only. Add this next to a Terrain component. One sculpt per Terrain.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Terrain Sculpt\u0022 )]\r\n[Category( \u0022SubTerrain\u0022 )]\r\n[Icon( \u0022landslide\u0022 )]\r\npublic sealed class TerrainSculpt : Component\r\n{\r\n\tconst int MinTexelsAcross = 8;\r\n\r\n\t[Property, Group( \u0022Limits\u0022 ), Range( 16f, 512f )]\r\n\tpublic float MaxRadius { get; set; } = 240f;\r\n\r\n\t[Property, Group( \u0022Limits\u0022 ), Range( 0f, 64f )]\r\n\tpublic float FloorMargin { get; set; } = 16f;\r\n\r\n\t[Property, Group( \u0022Limits\u0022 ), Range( 64f, 2048f )]\r\n\tpublic float GroundDrop { get; set; } = 512f;\r\n\r\n\t[Property, Group( \u0022Crater\u0022 )]\r\n\tpublic CraterBrushSettings Crater { get; set; } = new();\r\n\r\n\t[Property, Group( \u0022Dig\u0022 )]\r\n\tpublic DigBrushSettings Dig { get; set; } = new();\r\n\r\n\t[Property, Group( \u0022Flatten\u0022 )]\r\n\tpublic FlattenBrushSettings Flatten { get; set; } = new();\r\n\r\n\t[Property, Group( \u0022Paint\u0022 )]\r\n\tpublic bool PaintDirt { get; set; }\r\n\r\n\t[Property, Group( \u0022Paint\u0022 ), Range( 0, 31 )]\r\n\tpublic int PaintMaterialIndex { get; set; } = 1;\r\n\r\n\t[Property, Group( \u0022Paint\u0022 ), Range( 0f, 1f )]\r\n\tpublic float PaintStrength { get; set; } = 0.85f;\r\n\r\n\t[Property, Group( \u0022Perf\u0022 ), Range( 1, 16 )]\r\n\tpublic int StampsPerFrame { get; set; } = 4;\r\n\r\n\t[Property, Group( \u0022Debug\u0022 )]\r\n\tpublic bool DebugLog { get; set; } = true;\r\n\r\n\tpublic Action\u003CTerrainEditResult\u003E OnApplied { get; set; }\r\n\r\n\t/// \u003Csummary\u003EFired after a successful play-mode reset (Clear Edits / ResetAll). Debris listens here.\u003C/summary\u003E\r\n\tpublic Action OnReset { get; set; }\r\n\r\n\treadonly List\u003CTerrainEditRequest\u003E _queue = new();\r\n\treadonly List\u003CTerrainEditResult\u003E _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? $\u0022snapshot ready res={GetTerrain()?.Storage?.Resolution} size={GetTerrain()?.Storage?.TerrainSize} height={GetTerrain()?.Storage?.TerrainHeight} texels={_baseHeight?.Length}\u0022\r\n\t\t\t: \u0022snapshot FAILED \u2014 no Terrain/Storage on this object\u0022 );\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/// \u003Csummary\u003EQueue an edit. Returns whether it was accepted (flushes next fixed update).\u003C/summary\u003E\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( \u0022[Sculpt] TryApply \u2014 scene invalid\u0022 );\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( \u0022[Sculpt] TryApply ignored \u2014 not playing\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\t\tif ( request.Radius \u003C= 0f )\r\n\t\t{\r\n\t\t\tLog.Info( $\u0022[Sculpt] TryApply ignored \u2014 radius={request.Radius}\u0022 );\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\u003CTerrainSculpt\u003E() )\r\n\t\t{\r\n\t\t\tcandidates\u002B\u002B;\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( \u0022TryApply skip \u2014 no Terrain/Storage\u0022 );\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( $\u0022TryApply miss project pos={request.WorldPos} drop={sculpt.GroundDrop}\u0022 );\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 \u003E= 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( $\u0022[Sculpt] TryApply \u2014 no terrain under {request.WorldPos} (components={candidates})\u0022 );\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( $\u0022TryApply hit kind={request.Kind} dist={MathF.Sqrt( bestDist ):0.#} pos={request.WorldPos}\u0022 );\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( \u0022[Sculpt] ResetAll \u2014 scene invalid\u0022 );\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\u003CTerrainSculpt\u003E() )\r\n\t\t{\r\n\t\t\tif ( sculpt.ResetEdits() )\r\n\t\t\t\tcleared\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tLog.Info( $\u0022[Sculpt] ResetAll \u2014 cleared {cleared} terrain(s)\u0022 );\r\n\t\treturn cleared;\r\n\t}\r\n\r\n\t[Button( \u0022Clear Edits\u0022 )]\r\n\tpublic void ClearEdits()\r\n\t{\r\n\t\tResetEdits();\r\n\t}\r\n\r\n\t[Button( \u0022Clear All Scene Edits\u0022 )]\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( $\u0022[Sculpt] {GameObject.Name}: Clear Edits \u2014 no snapshot (play first, or already restored)\u0022 );\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( $\u0022[Sculpt] {GameObject.Name}: Clear Edits \u2014 no Terrain/Storage\u0022 );\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( $\u0022[Sculpt] {GameObject.Name}: Clear Edits \u2014 size mismatch snapshot={_baseHeight.Length} map={storage.HeightMap.Length}\u0022 );\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 \u0026\u0026 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( $\u0022[Sculpt] {GameObject.Name}: edits cleared\u0022 );\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( $\u0022ApplyQueued \u2014 dropped {_queue.Count} stamps (no Terrain/Storage)\u0022 );\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( \u0022ApplyQueued \u2014 snapshot failed\u0022 );\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 \u003C take; i\u002B\u002B )\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( \u0022ApplyQueued \u2014 all stamps missed terrain\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tPushToEngine( terrain, dirty, _paintedThisApply );\r\n\t\tDbg( $\u0022pushed dirty=({dirty.Left},{dirty.Top},{dirty.Width}x{dirty.Height}) painted={_paintedThisApply} remaining={_queue.Count}\u0022 );\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 \u003C _appliedThisFlush.Count; i\u002B\u002B )\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 \u0026\u0026 depth \u003C= 0f )\r\n\t\t{\r\n\t\t\tDbg( $\u0022ApplyStamp \u2014 depth 0 kind={request.Kind}\u0022 );\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( $\u0022ApplyStamp \u2014 project miss world={request.WorldPos}\u0022 );\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 \u003C= 0f || height \u003C= 0f || res \u003C= 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 \u003C 0f || uv.y \u003C 0f || uv.x \u003E 1f || uv.y \u003E 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 \u003C MinTexelsAcross \u0026\u0026 !_warnedLowRes )\r\n\t\t{\r\n\t\t\t_warnedLowRes = true;\r\n\t\t\tLog.Warning( $\u0022[TerrainSculpt] Radius {radius:0} spans {across:0.0} texels (need \u003E={MinTexelsAcross} to hide a character). texelSize={texelSize:0.0}.\u0022 );\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 =\u003E Math.Clamp( Dig.Irregularity, 0f, 0.4f ),\r\n\t\t\tTerrainEditKind.Flatten =\u003E 0f,\r\n\t\t\t_ =\u003E 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 =\u003E Math.Clamp( Dig.Stretch, 0f, 0.35f ),\r\n\t\t\tTerrainEditKind.Flatten =\u003E 0f,\r\n\t\t\t_ =\u003E 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 =\u003E Math.Clamp( Dig.Blend, 0f, 1f ),\r\n\t\t\tTerrainEditKind.Flatten =\u003E 0f,\r\n\t\t\t_ =\u003E Math.Clamp( Crater.Blend, 0f, 1f )\r\n\t\t};\r\n\t\tvar shapePad = 1f \u002B irreg \u002B stretchAmt \u002B 0.25f \u002B blend * 0.15f;\r\n\t\tvar radTexels = Math.Max( 1, (int)Math.Ceiling( radius / texelSize * shapePad ) \u002B 3 );\r\n\t\tdirty = new RectInt( cx - radTexels, cy - radTexels, radTexels * 2 \u002B 1, radTexels * 2 \u002B 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 \u003C= x0 || y1 \u003C= 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 \u0026\u0026 PaintMaterialIndex \u003E= 0\r\n\t\t\t\u0026\u0026 storage.Materials != null\r\n\t\t\t\u0026\u0026 PaintMaterialIndex \u003C 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 \u003C 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 \u002B rng.Float( -stretchAmt, stretchAmt ), 0.55f, 1.7f );\r\n\t\tif ( stretchAmt \u003C= 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 \u002B 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 \u0026\u0026 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 \u002B 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 \u002B MathF.Cos( eccA ) * ecc;\r\n\t\t\tctx.OriginY = originY \u002B 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 \u003E 0.01f \u0026\u0026 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( $\u0022stamp 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}-\u003E{heightAfter} drop={dropWorld:0.0}u wrote={ctx.Written} rect=({dirty.Left},{dirty.Top},{dirty.Width}x{dirty.Height})\u0022 );\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 \u003C 3 || hgt \u003C 3 )\r\n\t\t\treturn;\r\n\r\n\t\tvar need = w * hgt;\r\n\t\tif ( _mergeScratch == null || _mergeScratch.Length \u003C 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 \u003C hgt; y\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar src = (y0 \u002B y) * res \u002B 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 \u003C hgt - 1; y\u002B\u002B )\r\n\t\t{\r\n\t\t\tfor ( var x = 1; x \u003C w - 1; x\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar i = (y0 \u002B y) * res \u002B (x0 \u002B x);\r\n\t\t\t\tvar row = y * w \u002B x;\r\n\t\t\t\tvar left = scratch[row - 1];\r\n\t\t\t\tvar right = scratch[row \u002B 1];\r\n\t\t\t\tvar down = scratch[row - w];\r\n\t\t\t\tvar up = scratch[row \u002B w];\r\n\t\t\t\tvar leftD = (int)_baseHeight[i - 1] - left \u003E= dugU;\r\n\t\t\t\tvar rightD = (int)_baseHeight[i \u002B 1] - right \u003E= dugU;\r\n\t\t\t\tvar downD = (int)_baseHeight[i - res] - down \u003E= dugU;\r\n\t\t\t\tvar upD = (int)_baseHeight[i \u002B res] - up \u003E= dugU;\r\n\t\t\t\tif ( !((leftD \u0026\u0026 rightD) || (downD \u0026\u0026 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 \u003E= 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 \u002B (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 \u002B (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 \u002B Vector3.Up * 8f;\r\n\t\tvar ray = new Ray( origin, Vector3.Down );\r\n\t\tif ( terrain.RayIntersects( ray, GroundDrop \u002B 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 \u003E= 0f \u0026\u0026 local.y \u003E= 0f \u0026\u0026 local.x \u003C= size \u0026\u0026 local.y \u003C= 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 \u003C 1 || region.Height \u003C 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\u003CClutterGridSystem\u003E();\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 \u0026\u0026 _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 ) \u002B 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 \u003C _baseHeight.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _baseHeight[i] \u003C min )\r\n\t\t\t\tmin = _baseHeight[i];\r\n\t\t}\r\n\r\n\t\tif ( min \u003E= needU )\r\n\t\t{\r\n\t\t\tDbg( $\u0022floor ok min={min} need={needU} ({needWorld:0.#}u of {height:0.#})\u0022 );\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 \u003C _baseHeight.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar v = (int)_baseHeight[i] \u002B 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( $\u0022raised floor pad={pad} ({padWorld:0.#}u) min {min}-\u003E{min \u002B pad} need={needU} z {_assetZ:0.#}-\u003E{WorldPosition.z:0.#}\u0022 );\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 \u0026\u0026 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( \u0022RestoreFromSnapshot \u2014 disk heightmap restored\u0022 );\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 \u003E 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\u003CTerrain\u003E( FindMode.EverythingInSelfAndDescendants );\r\n\t}\r\n\r\n\tstatic RectInt FullRect( int resolution ) =\u003E new( 0, 0, resolution, resolution );\r\n\r\n\tstatic int WorldToUshort( float world, float terrainHeight )\r\n\t{\r\n\t\tif ( terrainHeight \u003C= 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( $\u0022[Sculpt] {GameObject.Name}: {message}\u0022 );\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( \u0022Depth Scale\u0022 ), 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( \u0022Depth Scale\u0022 ), 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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 = \u00221.4.15\u0022;\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/// \u003Csummary\u003EWear only. Move mix on the pair that is already there. The selected tmat is not written.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Keep pair\u0022 ), WideMode]\r\n\tpublic bool MixOnly { get; set; }\r\n\r\n\t/// \u003Csummary\u003EAlt-click the ground to stay on. Wear will not paint other bases.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Stay on sampled\u0022 ), WideMode]\r\n\tpublic bool LimitToSampledGround { get; set; }\r\n\r\n\t/// \u003Csummary\u003EPath only. Hard brush center packs the path tmat as base.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Pack center\u0022 ), Description( \u0022On: solid path core along the stroke, soft apron outside. Off: soft ground \u002B path apron only.\u0022 ), WideMode]\r\n\tpublic bool PackCenter { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EPath \u002B Pack center. Higher = wider solid middle. Outer ring stays soft blend.\u003C/summary\u003E\r\n\t[Property, Title( \u0022Core\u0022 ), Description( \u0022How much of the brush packs solid. Raise for a wider solid middle. Outer ring stays Center mix apron.\u0022 ), Range( PackCoreMin, PackCoreMax ), Step( 0.01f ), WideMode]\r\n\tpublic float PackCore { get; set; } = 0.55f;\r\n\r\n\t[Property, Title( \u0022How worn\u0022 ), Description( \u0022Overlay mix. Default 0.28. Soft chew to 0.36; hard stop 0.40. Above 0.36 can dither.\u0022 ), Range( 0.05f, WearMixMax ), Step( 0.01f ), WideMode]\r\n\tpublic float HowWorn\r\n\t{\r\n\t\tget =\u003E MixCap;\r\n\t\tset =\u003E MixCap = value;\r\n\t}\r\n\r\n\t[Property, Title( \u0022Center mix\u0022 ), Description( \u0022Soft apron overlay mix outside the packed core. Raise with brush Size for a wider blend.\u0022 ), Range( 0.05f, PathMixMax ), Step( 0.01f ), WideMode]\r\n\tpublic float CenterMix\r\n\t{\r\n\t\tget =\u003E MixCap;\r\n\t\tset =\u003E MixCap = value;\r\n\t}\r\n\r\n\tpublic bool OnlyOnSlope { get; set; } = true;\r\n\r\n\t[Property, Title( \u0022Steeper than\u0022 ), Description( \u0022How steep the ground must be. 20 is a hill face. Lower it to catch gentler banks.\u0022 ), 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 =\u003E SampledGroundIndex \u003E= 0;\r\n\r\n\tpublic bool HasPathGround =\u003E PathGroundIndex \u003E= 0;\r\n\r\n\tpublic bool Dithers =\u003E MixCap \u003E DitherWarn;\r\n\r\n\t/// \u003Csummary\u003EStamp value above which Pack center writes solid path. Higher Core = lower threshold.\u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Per Ground / Wear / Path mix and gate, for the editor session.\r\n/// \u003C/summary\u003E\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\u003CSubTerrainPaintLayer, LayerState\u003E 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n[Title( \u0022Terrain Sculpt Net\u0022 )]\r\n[Category( \u0022SubTerrain\u0022 )]\r\n[Icon( \u0022share\u0022 )]\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n[Title( \u0022Terrain Debris FX\u0022 )]\r\n[Category( \u0022SubTerrain\u0022 )]\r\n[Icon( \u0022grain\u0022 )]\r\npublic sealed class TerrainDebrisFx : Component\r\n{\r\n\tconst string DefaultClodPath = \u0022prefabs/subterrain/dirt_clod.prefab\u0022;\r\n\tconst string DefaultSprayPath = \u0022prefabs/subterrain/dirt_spray.prefab\u0022;\r\n\tconst FindMode RigidbodyFind = FindMode.Enabled | FindMode.InSelf | FindMode.InDescendants;\r\n\r\n\t[Property, Group( \u0022Source\u0022 )]\r\n\tpublic TerrainSculpt Sculpt { get; set; }\r\n\r\n\t[Property, Group( \u0022Prefabs\u0022 )]\r\n\tpublic PrefabFile ClodPrefab { get; set; }\r\n\r\n\t[Property, Group( \u0022Prefabs\u0022 )]\r\n\tpublic PrefabFile SprayPrefab { get; set; }\r\n\r\n\t[Property, Group( \u0022Kinds\u0022 )]\r\n\tpublic bool Craters { get; set; } = true;\r\n\r\n\t[Property, Group( \u0022Kinds\u0022 )]\r\n\tpublic bool Digs { get; set; } = true;\r\n\r\n\t[Property, Group( \u0022Clods\u0022 ), Range( 0, 48 )]\r\n\tpublic int MinClods { get; set; } = 8;\r\n\r\n\t[Property, Group( \u0022Clods\u0022 ), Range( 1, 64 )]\r\n\tpublic int MaxClods { get; set; } = 24;\r\n\r\n\t[Property, Group( \u0022Clods\u0022 ), Range( 0f, 0.5f )]\r\n\tpublic float ClodsPerRadius { get; set; } = 0.12f;\r\n\r\n\t[Property, Group( \u0022Clods\u0022 ), Range( 0, 128 )]\r\n\tpublic int MaxAlive { get; set; } = 64;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0f, 4000f )]\r\n\tpublic float Impulse { get; set; } = 640f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0f, 3f )]\r\n\tpublic float UpBias { get; set; } = 0.45f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0.5f, 4f )]\r\n\tpublic float Outward { get; set; } = 2.2f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0.4f, 1.4f )]\r\n\tpublic float RimMin { get; set; } = 0.78f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0.5f, 1.6f )]\r\n\tpublic float RimMax { get; set; } = 1.12f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0f, 40f )]\r\n\tpublic float Spin { get; set; } = 14f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0f, 48f )]\r\n\tpublic float SpawnLift { get; set; } = 20f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0.05f, 1.5f )]\r\n\tpublic float ScaleMin { get; set; } = 0.7f;\r\n\r\n\t[Property, Group( \u0022Launch\u0022 ), Range( 0.05f, 2.5f )]\r\n\tpublic float ScaleMax { get; set; } = 1.35f;\r\n\r\n\t[Property, Group( \u0022Dig\u0022 ), Range( 0.1f, 1f )]\r\n\tpublic float DigImpulseScale { get; set; } = 0.5f;\r\n\r\n\t[Property, Group( \u0022Debug\u0022 )]\r\n\tpublic bool DebugLog { get; set; }\r\n\r\n\treadonly List\u003CGameObject\u003E _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\u003CTerrainSculpt\u003E( FindMode.EnabledInSelf );\r\n\t\tClodPrefab ??= ResourceLibrary.Get\u003CPrefabFile\u003E( DefaultClodPath );\r\n\t\tSprayPrefab ??= ResourceLibrary.Get\u003CPrefabFile\u003E( DefaultSprayPath );\r\n\r\n\t\tif ( Sculpt == null || !Sculpt.IsValid() )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[Debris] {GameObject.Name}: no TerrainSculpt on this object\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tSculpt.OnApplied \u002B= OnTerrainApplied;\r\n\t\tSculpt.OnReset \u002B= 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 \u0026\u0026 !Craters )\r\n\t\t\treturn;\r\n\r\n\t\tif ( result.Kind == TerrainEditKind.Dig \u0026\u0026 !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 \u003C count; i\u002B\u002B )\r\n\t\t\tSpawnClod( result, impulse, up, i, count );\r\n\r\n\t\tDbg( $\u0022spawned {count} clods kind={result.Kind} r={result.Radius:0.#}\u0022 );\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 )) \u002B 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 \u002B 0.05f, RimMax );\r\n\t\tvar radial = result.Radius * Random.Shared.Float( rimMin, rimMax );\r\n\t\tvar pos = result.Origin \u002B outward * radial \u002B 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\u003CRigidbody\u003E( RigidbodyFind );\r\n\t\tif ( !body.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar dir = (outward * Outward \u002B 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 \u003E 0 \u0026\u0026 _alive.Count \u003E 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 \u003E= 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 \u003E= 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( $\u0022[Debris] {message}\u0022 );\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/// \u003Csummary\u003E\r\n/// Shared stroke logic for SubTerrain sculpt tools.\r\n/// \u003C/summary\u003E\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\u003CBrushData\u003E _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\u003CTerrain\u003E();\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 \u002B 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 \u0026\u0026 !brush.RandomRotation )\r\n\t\t\t{\r\n\t\t\t\tbrush.Rotation = ((brush.Rotation \u002B d.x * 0.5f) % 360f \u002B 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 \u0026\u0026 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 \u002B t * 58f;\r\n\t\t\t\t\t_raiseStampAccum \u002B= Math.Clamp( RealTime.Delta, 0f, 0.1f ) * stampsPerSec;\r\n\t\t\t\t\twhile ( _raiseStampAccum \u003E= 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/// \u003Csummary\u003E\r\n\t/// World-space stamp fill so fast / zoomed-out drags do not leave dashed strokes.\r\n\t/// \u003C/summary\u003E\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 \u003C 0.001f )\r\n\t\t\treturn;\r\n\r\n\t\tif ( dist \u003C= 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 \u003E 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 \u003C= stamps; i\u002B\u002B )\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 = \u0022Roboto Mono\u0022,\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 = $\u0022Size: {brush.Size}\u0022;\r\n\t\t\tGizmo.Draw.ScreenText( textScope, _lastHitWorldPos, offset );\r\n\r\n\t\t\ttextScope.Text = $\u0022Opacity: {brush.Opacity:0.##}\u0022;\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 = $\u0022Speed: {brush.Speed}\u0022;\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 \u0026\u0026 !brush.RandomRotation )\r\n\t\t{\r\n\t\t\ttextScope.Text = $\u0022Rotation: {brush.Rotation:0.#}\u0022;\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/// \u003Csummary\u003E\r\n\t/// World Size is brush radius; dispatch covers the diameter (Size * 2 in world units).\r\n\t/// \u003C/summary\u003E\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 \u002B 1, size \u002B 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 \u0026\u0026 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 \u003C= (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( \u0022terrain/cs_subterrain_sculpt\u0022 );\r\n\r\n\t\tcs.Attributes.SetComboEnum( \u0022D_SCULPT_MODE\u0022, Mode );\r\n\r\n\t\tcs.Attributes.Set( \u0022Heightmap\u0022, terrain.HeightMap );\r\n\t\tcs.Attributes.Set( \u0022ControlMap\u0022, terrain.ControlMap );\r\n\r\n\t\tcs.Attributes.Set( \u0022HeightUV\u0022, paint.HitUV );\r\n\t\tcs.Attributes.Set( \u0022FlattenHeight\u0022, paint.FlattenHeight );\r\n\t\tcs.Attributes.Set( \u0022SlopeStartUV\u0022, SlopeStartUV );\r\n\t\tcs.Attributes.Set( \u0022SlopeEndUV\u0022, paint.HitUV );\r\n\t\tcs.Attributes.Set( \u0022SlopeStartHeight\u0022, SlopeStartHeight01 );\r\n\t\tcs.Attributes.Set( \u0022SlopeEndHeight\u0022, paint.FlattenHeight );\r\n\t\tcs.Attributes.Set( \u0022BrushStrength\u0022, strength );\r\n\t\tcs.Attributes.Set( \u0022BrushSize\u0022, size );\r\n\t\tcs.Attributes.Set( \u0022Brush\u0022, 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tvoid DispatchEngineSculpt( Terrain terrain, SubTerrainPaintParameters paint, Texture brushTexture, int size, float strength )\r\n\t{\r\n\t\tvar cs = new ComputeShader( \u0022terrain/cs_terrain_sculpt\u0022 );\r\n\r\n\t\tcs.Attributes.SetComboEnum( \u0022D_SCULPT_MODE\u0022, (SculptMode)(int)Mode );\r\n\r\n\t\tcs.Attributes.Set( \u0022Heightmap\u0022, terrain.HeightMap );\r\n\t\tcs.Attributes.Set( \u0022ControlMap\u0022, terrain.ControlMap );\r\n\r\n\t\t_engineBrushBuffer ??= new GpuBuffer\u003CBrushData\u003E( 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( \u0022BrushSettings\u0022, _engineBrushBuffer );\r\n\t\tcs.Attributes.Set( \u0022Brush\u0022, 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tprotected virtual void ApplyExtraAttributes( ComputeShader cs, SubTerrainPaintParameters paint )\r\n\t{\r\n\t}\r\n\r\n\tprivate static T[] CopyRegion\u003CT\u003E( 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 \u003C rect.Height; y\u002B\u002B )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x \u003C rect.Width; x\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tregion[x \u002B y * rect.Width] = data[rect.Left \u002B x \u002B (rect.Top \u002B 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\u003CT\u003E( Terrain terrain, T[] dest, T[] region, RectInt dirtyRegion, Terrain.SyncFlags flags ) =\u003E () =\u003E\r\n\t{\r\n\t\tif ( !terrain.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tfor ( int y = 0; y \u003C dirtyRegion.Height; y\u002B\u002B )\r\n\t\t{\r\n\t\t\tfor ( int x = 0; x \u003C dirtyRegion.Width; x\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tdest[dirtyRegion.Left \u002B x \u002B (dirtyRegion.Top \u002B y) * terrain.Storage.Resolution] = region[x \u002B 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 = $\u0022Terrain {DisplayInfo.For( this ).Name}\u0022;\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/// \u003Csummary\u003E\r\n/// Destroys this GameObject after a delay. Used by debris prefabs.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Timed Destroy\u0022 )]\r\n[Category( \u0022SubTerrain\u0022 )]\r\n[Icon( \u0022timer\u0022 )]\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 \u003E= 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/// \u003Csummary\u003E\r\n/// Destroys this GameObject after a delay. Used by debris prefabs.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Timed Destroy\u0022 )]\r\n[Category( \u0022SubTerrain\u0022 )]\r\n[Icon( \u0022timer\u0022 )]\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 \u003E= 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/// \u003Csummary\u003E\r\n/// Keeps the click height along the stroke. Paint Texture Path is the control-map path.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Path\u0022 )]\r\n[Icon( \u0022route\u0022 )]\r\n[Alias( \u0022subterrain_path\u0022 )]\r\n[Group( \u00221\u0022 )]\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003EFraction of the slider track for the fine band (8..FineMax).\u003C/summary\u003E\r\n\tpublic const float FineTrack = 0.70f;\r\n\r\n\t/// \u003Csummary\u003EEnd of mid band on the track (FineMax..MidMax). Coarse is the rest to 1.\u003C/summary\u003E\r\n\tpublic const float MidTrack = 0.90f;\r\n\r\n\tpublic static int Clamp( int size ) =\u003E Math.Clamp( size, Min, Max );\r\n\r\n\tpublic static int StepFor( int size )\r\n\t{\r\n\t\tif ( size \u003C FineMax )\r\n\t\t\treturn FineStep;\r\n\t\tif ( size \u003C 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 \u002B 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 \u003C= FineTrack )\r\n\t\t{\r\n\t\t\tfloat u = FineTrack \u003C= 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 \u003C= 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 \u003C= 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 \u003C= MidMax )\r\n\t\t{\r\n\t\t\tfloat u = (size - FineMax) / (float)(MidMax - FineMax);\r\n\t\t\treturn FineTrack \u002B 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 \u002B 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 ) =\u003E a \u002B (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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n[EditorTool( \u0022tools.subterrain\u0022 )]\r\n[Title( \u0022SubTerrain\u0022 )]\r\n[Icon( \u0022terrain\u0022 )]\r\n[Alias( \u0022subterrain\u0022 )]\r\n[Group( \u0022Scene\u0022 )]\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\u003CTerrain\u003E();\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 \u0026\u0026 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 = \u0022Paint Texture.\u0022;\r\n\t\t\t_intentLabel.ToolTip = \u0022Alt sample | Ctrl fade | Shift smooth | ring yellow/green = Alt/Shift. Modes and size keys are in Shortcuts below.\u0022;\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 ) =\u003E \u0022Smooth height.\u0022,\r\n\t\t\tnameof( SubPlainerTool ) =\u003E \u0022Plainer: broad weighted leveling.\u0022,\r\n\t\t\tnameof( SubFlattenTool ) =\u003E \u0022Flatten to click Z.\u0022,\r\n\t\t\tnameof( SubSlopeTool ) =\u003E \u0022Slope: click start, drag to end.\u0022,\r\n\t\t\tnameof( SubPathTool ) =\u003E \u0022Path: constant elevation strip.\u0022,\r\n\t\t\tnameof( SubSetHeightTool ) =\u003E \u0022Flatten to Target Height.\u0022,\r\n\t\t\tnameof( SubNoiseTool ) =\u003E \u0022Noise bump. Ctrl inverts.\u0022,\r\n\t\t\tnameof( SubHoleTool ) =\u003E \u0022Punch hole. Ctrl fills.\u0022,\r\n\t\t\tnameof( SubBridgeTool ) =\u003E \u0022Bridge: press start, release on end.\u0022,\r\n\t\t\t_ =\u003E \u0022Raise height. Ctrl lowers.\u0022\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 ) =\u003E \u0022Height-only strip. Control-map Path is Paint Texture (Ctrl\u002B3).\u0022,\r\n\t\t\tnameof( SubSetHeightTool ) =\u003E \u0022Alt-click loads Z into Target Height.\u0022,\r\n\t\t\tnameof( SubPlainerTool ) =\u003E \u0022Like Hammer displacement smoothing across a wide brush.\u0022,\r\n\t\t\tnameof( SubBridgeTool ) =\u003E \u0022Deck sits on the line between the two picked heights. Shortcut B.\u0022,\r\n\t\t\t_ =\u003E null\r\n\t\t};\r\n\r\n\t\tvar keys = \u0022Left / Right or Ctrl\u002B[ / ] brush size. N activates SubTerrain. Keys 1-9, 0 Plainer, B Bridge.\u0022;\r\n\t\treturn tip is null ? keys : $\u0022{tip} {keys}\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tpublic void DetachSidebarForReuse()\r\n\t{\r\n\t\tif ( _sidebar is not null \u0026\u0026 _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/// \u003Csummary\u003E\r\n\t/// Sculpt modes only. Paint Texture keeps Up / Down for tmat selection.\r\n\t/// \u003C/summary\u003E\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 \u002B 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/// \u003Csummary\u003E\r\n\t/// Cycle selected paint tmat. Path skips the locked ground row (that role is Alt-click).\r\n\t/// \u003C/summary\u003E\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 \u003C= 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 \u003C 0 || current \u003E= count )\r\n\t\t\tcurrent = 0;\r\n\r\n\t\tfor ( int step = 0; step \u003C count; step\u002B\u002B )\r\n\t\t{\r\n\t\t\tcurrent = (current \u002B delta) % count;\r\n\t\t\tif ( current \u003C 0 )\r\n\t\t\t\tcurrent \u002B= 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 =\u003E TerrainEditorTool.BrushList;\r\n\r\n\tpublic Texture SelectedBrushTexture =\u003E 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\u003CEditorTool\u003E 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\u003CTerrain\u003E();\r\n\t\tif ( !selectedTerrain.IsValid() )\r\n\t\t{\r\n\t\t\tSelection.Clear();\r\n\t\t\tvar first = Scene.GetAllComponents\u003CTerrain\u003E().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 \u0026\u0026 _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() =\u003E new SubTerrainToolShortcutsWidget();\r\n\r\n\t[Shortcut( \u0022tools.subterrain\u0022, \u0022n\u0022, 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 \u0026\u0026 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 \u0026\u0026 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( \u0022SubTerrain\u0022, \u0022terrain\u0022 );\r\n\t\tsidebar.ToolTip = $\u0022SubTerrain {SubTerrainPaintSettings.LibraryVersion}\u0022;\r\n\r\n\t\t_intentLabel = sidebar.Layout.Add( new Label( \u0022\u0022 ) );\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( \u0022Brush Type\u0022 );\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( \u0022Brush Properties\u0022 );\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\u003CTerrain\u003E().IsValid() )\r\n\t\t{\r\n\t\t\tvar group = sidebar.AddGroup( \u0022No Terrain\u0022 );\r\n\t\t\tgroup.Add( new Label( \u0022Select a GameObject with a Terrain component.\u0022 ) );\r\n\t\t}\r\n\r\n\t\t{\r\n\t\t\tvar cursorLayout = sidebar.AddGroup( \u0022Cursor\u0022 );\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(\u0022Activate SubTerrain\u0022, \u0022N\u0022),\r\n\t\t\t(\u0022Sculpt / paint tools\u0022, \u00221 - 9\u0022),\r\n\t\t\t(\u0022Plainer / Bridge\u0022, \u00220 / B\u0022),\r\n\t\t\t(\u0022Paint: Ground / Wear / Path\u0022, \u0022Ctrl\u002B1 / 2 / 3\u0022),\r\n\t\t\t(\u0022Paint tmat prev / next\u0022, \u0022Up / Down\u0022),\r\n\t\t\t(\u0022Opacity (sculpt)\u0022, \u0022Up / Down\u0022),\r\n\t\t\t(\u0022Brush size\u0022, \u0022Left / Right | Ctrl\u002B[ / ]\u0022),\r\n\t\t\t(\u0022Size / opacity\u0022, \u0022MMB\u002BShift drag\u0022),\r\n\t\t\t(\u0022Brush rotation\u0022, \u0022MMB\u002BCtrl drag\u0022),\r\n\t\t\t(\u0022Sample / fade / smooth\u0022, \u0022Alt / Ctrl / Shift\u0022)\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 == \u0022SidebarGroupWidget\u0022 )\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 \u002B= 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 = \u0022How fast height accumulates. Hold still to keep raising. Higher = more stamps and more height per stamp.\u0022;\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? \u0022Bridge always builds at the exact start and end heights.\u0022\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 = \u0022How fast wear builds on this stroke. Ctrl reduces.\u0022;\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? \u0022Path uses Center mix and Core for the apron / solid ribbon. Pack center optional. Soft circle or stroke.\u0022\r\n\t\t\t: \u0022Ground paint lays a solid tmat. Mix is cleared.\u0022;\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 ) =\u003E \u0022Brush soft strength. Ctrl lowers. Use Speed for how fast height builds.\u0022,\r\n\t\t\tnameof( SubHoleTool ) =\u003E \u0022How fast the punch cuts. Ctrl fills the hole back.\u0022,\r\n\t\t\tnameof( SubNoiseTool ) =\u003E \u0022How strong the bump is. Ctrl inverts.\u0022,\r\n\t\t\tnameof( SubFlattenTool ) =\u003E \u0022How fast it levels to the click height.\u0022,\r\n\t\t\tnameof( SubSetHeightTool ) =\u003E \u0022How fast it levels to Target Height.\u0022,\r\n\t\t\tnameof( SubSlopeTool ) =\u003E \u0022How fast it grades to the ramp.\u0022,\r\n\t\t\tnameof( SubPathTool ) =\u003E \u0022How fast it levels to the click height.\u0022,\r\n\t\t\tnameof( SubSmoothTool ) =\u003E \u0022How strong the smooth is.\u0022,\r\n\t\t\tnameof( SubPlainerTool ) =\u003E \u0022How strong the weighted leveling is.\u0022,\r\n\t\t\t_ =\u003E \u0022Stroke strength.\u0022\r\n\t\t};\r\n\t}\r\n}\r\n\r\n[Title( \u0022Raise / Lower\u0022 )]\r\n[Description( \u0022Paint height up. Hold Ctrl to lower.\u0022 )]\r\n[Icon( \u0022height\u0022 )]\r\n[Alias( \u0022subterrain_raise_lower\u0022 )]\r\n[Group( \u00221\u0022 )]\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( \u0022Flatten\u0022 )]\r\n[Description( \u0022Level to the height under the click. Stays on that plane while you drag.\u0022 )]\r\n[Icon( \u0022trending_flat\u0022 )]\r\n[Alias( \u0022subterrain_flatten\u0022 )]\r\n[Group( \u00221\u0022 )]\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( \u0022Smooth\u0022 )]\r\n[Description( \u0022Blend neighbouring heights.\u0022 )]\r\n[Icon( \u0022rounded_corner\u0022 )]\r\n[Alias( \u0022subterrain_smooth\u0022 )]\r\n[Group( \u00221\u0022 )]\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( \u0022Plainer\u0022 )]\r\n[Description( \u0022Gradually level a broad weighted area, like Hammer displacement smoothing.\u0022 )]\r\n[Icon( \u0022blur_on\u0022 )]\r\n[Alias( \u0022subterrain_plainer\u0022 )]\r\n[Group( \u00221\u0022 )]\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( \u0022Noise\u0022 )]\r\n[Description( \u0022Add bump. Hold Ctrl to invert.\u0022 )]\r\n[Icon( \u0022grain\u0022 )]\r\n[Alias( \u0022subterrain_noise\u0022 )]\r\n[Group( \u00221\u0022 )]\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( \u0022Hole\u0022 )]\r\n[Description( \u0022Punch through the surface. Hold Ctrl to fill the hole back.\u0022 )]\r\n[Icon( \u0022trip_origin\u0022 )]\r\n[Alias( \u0022subterrain_hole\u0022 )]\r\n[Group( \u00221\u0022 )]\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 \u002B= () =\u003E\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\u003Cobject\u003E() );\r\n\t\tlist.ItemSize = new Vector2( 40, 40 );\r\n\t\tlist.ItemAlign = Sandbox.UI.Align.SpaceBetween;\r\n\t\tlist.OnPaintOverride \u002B= () =\u003E PaintListBackground( list );\r\n\t\tlist.ItemPaint = PaintBrushItem;\r\n\t\tlist.ItemSelected = item =\u003E\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( \u0022Brushes\u0022 );\r\n\t\tlabel.SetStyles( \u0022font-weight: bold\u0022 );\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( \u0022tools.subterrain.raise-lower\u0022, \u00221\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateRaiseLower() =\u003E EditorToolManager.SetSubTool( nameof( SubRaiseLowerTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.smooth\u0022, \u00222\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateSmooth() =\u003E EditorToolManager.SetSubTool( nameof( SubSmoothTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.flatten\u0022, \u00223\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateFlatten() =\u003E EditorToolManager.SetSubTool( nameof( SubFlattenTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.paint\u0022, \u00224\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivatePaint() =\u003E EditorToolManager.SetSubTool( nameof( SubTerrainPaintTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.slope\u0022, \u00225\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateSlope() =\u003E EditorToolManager.SetSubTool( nameof( SubSlopeTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.path\u0022, \u00226\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivatePath() =\u003E EditorToolManager.SetSubTool( nameof( SubPathTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.set-height\u0022, \u00227\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateSetHeight() =\u003E EditorToolManager.SetSubTool( nameof( SubSetHeightTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.noise\u0022, \u00228\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateNoise() =\u003E EditorToolManager.SetSubTool( nameof( SubNoiseTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.hole\u0022, \u00229\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateHole() =\u003E EditorToolManager.SetSubTool( nameof( SubHoleTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.plainer\u0022, \u00220\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivatePlainer() =\u003E EditorToolManager.SetSubTool( nameof( SubPlainerTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.bridge\u0022, \u0022B\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void ActivateBridge() =\u003E EditorToolManager.SetSubTool( nameof( SubBridgeTool ) );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.paint-ground\u0022, \u0022CTRL\u002B1\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void PaintGround() =\u003E SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Base );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.paint-wear\u0022, \u0022CTRL\u002B2\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void PaintWear() =\u003E SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Overlay );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.paint-path\u0022, \u0022CTRL\u002B3\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void PaintPath() =\u003E SubTerrainTool.ActiveInstance?.TrySetPaintLayer( SubTerrainPaintLayer.Path );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.brush-smaller\u0022, \u0022CTRL\u002B[\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void BrushSmaller() =\u003E SubTerrainTool.ActiveInstance?.NudgeBrushSize( -1 );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.brush-larger\u0022, \u0022CTRL\u002B]\u0022, typeof( SceneViewWidget ) )]\r\n\tpublic void BrushLarger() =\u003E SubTerrainTool.ActiveInstance?.NudgeBrushSize( 1 );\r\n\r\n\t[Shortcut( \u0022tools.subterrain.brush-smaller-arrow\u0022, \u0022LEFT\u0022, 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( \u0022tools.subterrain.brush-larger-arrow\u0022, \u0022RIGHT\u0022, 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( \u0022tools.subterrain.paint-tmat-prev\u0022, \u0022UP\u0022, 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( \u0022tools.subterrain.paint-tmat-next\u0022, \u0022DOWN\u0022, 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/// \u003Csummary\u003E\r\n/// Brush Size row: piecewise track (fine under 3500, mid to 18000, coarse to 35000) plus type-in.\r\n/// \u003C/summary\u003E\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( \u0022Size\u0022 ) );\r\n\t\tlabel.MinimumWidth = 72;\r\n\t\tlabel.Color = Theme.TextControl;\r\n\t\tlabel.ToolTip = \u0022World units. Fine under 3500 (step 50), mid to 18000 (step 500), then to 35000 (step 1000). Left / Right or Ctrl\u002B[ / ] steps.\u0022;\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 = \u0022Type an exact size (8-35000).\u0022;\r\n\t\t_field.EditingFinished \u002B= 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 = \u0022Drag: fine under 3500, mid to 18000, coarse to 35000. Left / Right or Ctrl\u002B[ / ] use the same steps.\u0022;\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 \u002B track.Width * SubTerrainBrushSize.FineTrack;\r\n\t\tfloat midX = track.Left \u002B 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 \u002B 4f ) );\r\n\t\tPaint.DrawRect( new Rect( midX, track.Top - 2f, 1f, track.Height \u002B 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 \u002B 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"}]}