Editor/Roof/ArchRoofRuns.cs
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 = ArchLap.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 );
}