editor/AdvancedMappingTools/Geometry/GutterEditor.cs

A roof gutter generator for the editor's primitive tool system. Sweeps a channel profile and two side walls along a drawn path, then places downpipe cylinders at a set spacing.

Really Bad Code
namespace AdvancedMappingTools;

/// <summary>
/// A rain gutter along a roof edge: channel, side walls and downpipes at a set spacing.
/// </summary>
[ToolCategory( "Roofs" )]
[Title( "Gutter System" ), Icon( "water_drop" )]
public sealed class GutterEditor( PrimitiveTool tool ) : PathEditor( tool )
{
	protected override bool SnapsToLines => true;
	protected override bool CanClose => true;

	protected override int MinPoints => 2;

	[Property, Group( "Gutter" ), Range( 2, 64, slider: false ), WideMode]
	public float Width { get; set; } = 12.0f;

	[Property, Group( "Gutter" ), Range( 2, 64, slider: false ), WideMode]
	public float Depth { get; set; } = 10.0f;

	[Property, Group( "Gutter" ), Title( "Wall" ), Range( 0.5f, 12, slider: false ), WideMode]
	public float Thickness { get; set; } = 1.5f;

	[Property, Group( "Gutter" ), Title( "Downpipe Spacing" ), Range( 0, 1024, slider: false ), WideMode, Description( "Zero leaves the downpipes off." )]
	public float DownpipeSpacing { get; set; } = 192.0f;

	[Property, HideIf( nameof( DownpipeSpacing ), 0.0f ), Group( "Gutter" ), Title( "Downpipe Radius" ), Range( 0.5f, 8, slider: false ), WideMode]
	public float DownpipeRadius { get; set; } = 2.0f;

	[Property, HideIf( nameof( DownpipeSpacing ), 0.0f ), Group( "Gutter" ), Title( "Downpipe Drop" ), Range( 0, 512, slider: false ), WideMode]
	public float DownpipeDrop { get; set; } = 64.0f;

	protected override void BuildMesh( MeshKit kit )
	{
		var path = SamplePath();

		if ( path.Count < 2 ) return;

		var half = MathF.Max( Width, 2.0f ) * 0.5f;
		var up = MathF.Max( Depth, 2.0f );
		var t = Math.Clamp( Thickness, 0.5f, 12.0f );

		kit.Material = TrimMaterial;

		// Floor of the channel.
		SweepPath( kit, path, Closed, [new( -half, 0.0f ), new( half, 0.0f ), new( half, t ), new( -half, t )] );

		// Two side walls.
		SweepPath( kit, path, Closed, [new( -half, t ), new( -half + t, t ), new( -half + t, up ), new( -half, up )] );
		SweepPath( kit, path, Closed, [new( half - t, t ), new( half, t ), new( half, up ), new( half - t, up )] );

		if ( DownpipeSpacing <= 0.0f ) return;

		var radius = Math.Clamp( DownpipeRadius, 0.5f, 8.0f );
		var drop = MathF.Max( DownpipeDrop, 0.0f );

		foreach ( var (position, _) in WalkPath( path, Closed, MathF.Max( DownpipeSpacing, 16.0f ) ) )
			kit.Cylinder( position, radius, radius, -drop, 0.0f, 8 );
	}
}