Editor helper for roof gutter and eave run generation. It computes gutter run paths for different roof styles, handles seams where runs cross wall lines, breaks runs at abutments, and builds eave/valley runs (including sawtooth and shed styles).
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Single source for both the gutter builder and the downpipe placer - what you see is what a pipe taps.
public static partial class ArchRoofGen
{
public static List<ArchRunPath> GutterRuns( ArchRoofPart roof, ArchBuilding building, ArchKit kit, ArchPlan plan )
{
var runs = new List<ArchRunPath>();
foreach ( var region in new ArchRoofRegionService( building, roof ).Resolve() )
{
runs.AddRange( GutterRuns( roof, building, kit, plan, region ) );
}
return runs;
}
static List<ArchRunPath> GutterRuns( ArchRoofPart roof, ArchBuilding building, ArchKit kit, ArchPlan plan, ArchRoofRegion region )
{
var runs = new List<ArchRunPath>();
var outline = region.Outer;
if ( !roof.Gutters || roof.Parapet )
{
return runs;
}
var profile = kit.FindProfile( "gutter" );
var bite = ArchContact.Bite( kit );
// Bitten on both axes, or the channel back and rim z-fight the fascia.
var height = roof.BaseHeight - (profile?.Max.y ?? 0f) - bite;
var stand = (roof.Fascia ? ArchProfiles.FasciaDepth( kit ) : 0f) - bite;
var kinds = ArchKinds.Load();
var junctions = ArchCrossGableService.Resolve( building, kit, kinds );
var fittingAbutments = FittingAbutments( roof, building, plan, kit, junctions, kinds );
bool Abuts( Vector2 point, Vector2 outward ) => Buried( fittingAbutments, point + outward * ArchProbe.Step );
// Hip and flat shed off every edge, so the wrap includes the wing's inside corner.
if ( roof.Style is RoofStyle.Flat or RoofStyle.Hip )
{
foreach ( var loop in ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, roof.Overhang + stand ), fittingAbutments ) )
{
runs.AddRange( Runs( loop, height, Abuts ) );
}
return runs;
}
if ( roof.Style == RoofStyle.Gable )
{
foreach ( var loop in ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, roof.Overhang + stand ), fittingAbutments ) )
{
for ( var index = 0; index < loop.Count; index++ )
{
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
var outward = ArchRegion.Outward( from, to );
if ( Sheds( roof, RoofStyle.Gable, outward ) )
{
runs.Add( Edge( from, to, height ) );
}
}
}
return runs;
}
ArchFootprint.Bounds( outline, out var lo, out var hi );
var overhang = new Vector2( roof.Overhang, roof.Overhang );
var reach = new Vector2( roof.Overhang + stand, roof.Overhang + stand );
EaveRuns( roof, lo - overhang, hi + overhang, lo - reach, hi + reach, height, runs );
return runs.Select( run => Seamed( run, roof, outline ) ).ToList();
}
// Seam where the run crosses wall lines so the buried stretch can drop; points are collinear, mitres untouched.
static ArchRunPath Seamed( ArchRunPath run, ArchRoofPart roof, IReadOnlyList<Vector2> outline )
{
if ( run.Points.Count < 2 )
{
return run;
}
ArchFootprint.Bounds( outline, out var outlineMin, out var outlineMax );
var walls = new[]
{
(Sideways: true, At: outlineMin.x),
(Sideways: true, At: outlineMax.x),
(Sideways: false, At: outlineMin.y),
(Sideways: false, At: outlineMax.y)
};
var seamed = new List<Vector2>();
for ( var edge = 0; edge < run.Edges; edge++ )
{
var from = run.At( edge );
var to = run.At( edge + 1 );
seamed.Add( from );
var crossings = new List<float>();
foreach ( var wall in walls )
{
var start = wall.Sideways ? from.x : from.y;
var end = wall.Sideways ? to.x : to.y;
var span = end - start;
if ( MathF.Abs( span ) < 0.01f )
{
continue;
}
var along = (wall.At - start) / span;
if ( along > 0.01f && along < 0.99f )
{
crossings.Add( along );
}
}
foreach ( var along in crossings.OrderBy( value => value ) )
{
var seam = Vector2.Lerp( from, to, along );
if ( (seam - seamed[^1]).Length > 0.05f )
{
seamed.Add( seam );
}
}
}
if ( !run.Closed )
{
seamed.Add( run.Points[^1] );
}
return new ArchRunPath { Closed = run.Closed, Points = seamed, Height = run.Height };
}
// Broken at abutments so the channel stops there; whole loop = mitred corners.
static IEnumerable<ArchRunPath> Runs( IReadOnlyList<Vector2> loop, float height, Func<Vector2, Vector2, bool> abuts )
{
return ArchBrokenRun.Of( loop, abuts ).Level( height ).Resolve();
}
// Low edges only; runs sit on the fascia's outer face, wound so +x faces the roof.
static void EaveRuns( ArchRoofPart roof, Vector2 min, Vector2 max, Vector2 low, Vector2 high, float height, List<ArchRunPath> runs )
{
switch ( roof.Style )
{
case RoofStyle.Shed when roof.RidgeAlongX:
runs.Add( roof.Reversed
? Edge( new Vector2( high.x, high.y ), new Vector2( low.x, high.y ), height )
: Edge( new Vector2( low.x, low.y ), new Vector2( high.x, low.y ), height ) );
break;
case RoofStyle.Shed:
runs.Add( roof.Reversed
? Edge( new Vector2( high.x, low.y ), new Vector2( high.x, high.y ), height )
: Edge( new Vector2( low.x, high.y ), new Vector2( low.x, low.y ), height ) );
break;
case RoofStyle.Sawtooth:
SawtoothValleys( roof, min, max, low, high, height, runs );
break;
}
}
// Outermost bay is a real eave; the rest are valley gutters against the previous bay's glazing.
static void SawtoothValleys( ArchRoofPart roof, Vector2 min, Vector2 max, Vector2 low, Vector2 high, float height, List<ArchRunPath> runs )
{
var bays = Math.Max( 1, roof.SawtoothBays );
var alongX = !roof.RidgeAlongX;
var step = (alongX ? max.x - min.x : max.y - min.y) / bays;
for ( var bay = 0; bay < bays; bay++ )
{
if ( alongX )
{
var x = bay == 0 ? low.x : min.x + bay * step;
runs.Add( Edge( new Vector2( x, high.y ), new Vector2( x, low.y ), height ) );
continue;
}
var y = bay == 0 ? low.y : min.y + bay * step;
runs.Add( Edge( new Vector2( low.x, y ), new Vector2( high.x, y ), height ) );
}
}
static ArchRunPath Edge( Vector2 from, Vector2 to, float height ) => ArchRunPath.Between( from, to, height );
}