Editor/Tools/BridgeTool.cs
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.
using System;
using Sandbox;
using Editor.TerrainEditor;
namespace Editor.SubTerrain;
/// <summary>
/// Bridge tool. Press on the first point and release on the second to raise a land bridge
/// spanning the gap. The deck grades from the start ground height to the end ground height,
/// and the whole span is written once on release rather than painted stroke by stroke.
/// </summary>
[Title( "Bridge" )]
[Description( "Press start, release on end. Raises a land bridge between the two heights." )]
[Icon( "linear_scale" )]
[Alias( "subterrain_bridge" )]
[Group( "1" )]
[Order( 10 )]
public class SubBridgeTool : SubTerrainBaseBrushTool
{
Vector3 _startLocal;
Vector3 _endLocal;
Vector2 _endUV;
float _halfWidthTexels;
public SubBridgeTool( SubTerrainTool parent ) : base( parent )
{
Mode = SubTerrainSculptMode.Bridge;
}
protected override void OnStrokeStart( Terrain terrain, Vector3 hitPosition )
{
_startLocal = hitPosition;
_endLocal = hitPosition;
SlopeStartUV = new Vector2( hitPosition.x, hitPosition.y ) / terrain.Storage.TerrainSize;
SlopeStartHeight01 = hitPosition.z / terrain.Storage.TerrainHeight;
}
protected override void OnPaint( Terrain terrain, SubTerrainPaintParameters paint )
{
_endLocal = paint.HitPosition;
}
protected override void OnPaintEnded( Terrain terrain )
{
BuildBridge( terrain );
base.OnPaintEnded( terrain );
}
void BuildBridge( Terrain terrain )
{
var storage = terrain.Storage;
var settings = _parent.BrushSettings;
var startUV = new Vector2( _startLocal.x, _startLocal.y ) / storage.TerrainSize;
_endUV = new Vector2( _endLocal.x, _endLocal.y ) / storage.TerrainSize;
_halfWidthTexels = Math.Max( settings.BridgeWidth * 0.5f / storage.TerrainSize * storage.Resolution, 1.0f );
var spanTexels = (_endUV - startUV) * storage.Resolution;
if ( spanTexels.Length < 1.0f )
return;
int size = (int)Math.Ceiling( Math.Max( Math.Abs( spanTexels.x ), Math.Abs( spanTexels.y ) ) + _halfWidthTexels * 2.0f ) + 2;
size = Math.Clamp( size, 1, storage.Resolution );
SlopeStartUV = startUV;
SlopeStartHeight01 = _startLocal.z / storage.TerrainHeight;
var paint = new SubTerrainPaintParameters
{
HitPosition = _endLocal,
HitUV = (startUV + _endUV) * 0.5f,
FlattenHeight = _endLocal.z / storage.TerrainHeight,
BrushSettings = settings
};
var brushTexture = _parent.SelectedBrushTexture;
if ( brushTexture is null )
return;
DispatchSculpt( terrain, paint, brushTexture, size, 1.0f );
}
protected override void ApplyExtraAttributes( ComputeShader cs, SubTerrainPaintParameters paint )
{
var settings = paint.BrushSettings;
cs.Attributes.Set( "SlopeEndUV", _endUV );
cs.Attributes.Set( "SlopeEndHeight", paint.FlattenHeight );
cs.Attributes.Set( "BridgeHalfWidth", _halfWidthTexels );
cs.Attributes.Set( "BridgeDeckRatio", 1.0f - settings.BridgeEdgeFalloff );
cs.Attributes.Set( "BridgeRaiseOnly", settings.BridgeRaiseOnly ? 1 : 0 );
}
protected override void DrawToolOverlay( Terrain terrain, Vector3 worldCenter )
{
if ( !_dragging )
return;
var startWorld = terrain.WorldTransform.PointToWorld( _startLocal );
var span = worldCenter - startWorld;
var flat = new Vector3( span.x, span.y, 0.0f );
Gizmo.Draw.Color = Color.FromBytes( 250, 180, 120 );
Gizmo.Draw.Line( startWorld, worldCenter );
if ( flat.Length < 0.001f )
return;
var halfWidth = _parent.BrushSettings.BridgeWidth * 0.5f;
var side = new Vector3( -flat.y, flat.x, 0.0f ) / flat.Length * halfWidth;
Gizmo.Draw.Line( startWorld + side, worldCenter + side );
Gizmo.Draw.Line( startWorld - side, worldCenter - side );
Gizmo.Draw.Line( startWorld - side, startWorld + side );
Gizmo.Draw.Line( worldCenter - side, worldCenter + side );
}
}