Editor utility for generating gutter and downpipe geometry. It extrudes gutter profiles along roof runs, places downpipe parts snapped to nearest eave, computes wall anchoring, and generates downpipe geometry, outlets and fixings for rainwater and conduit runs.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchGutterGen
{
public static void Run( ArchMesh canvas, ArchRunPath run, ArchKit kit, ArchBrush brush )
{
var profile = kit.FindProfile( "gutter" );
if ( profile is null || !profile.IsUsable || run.Points.Count < 2 )
{
return;
}
canvas.Extrude( run.Raised(), profile, 1f, Rotation.Identity, brush, run.Closed );
Brackets( canvas, run, profile, kit, brush );
}
// Extruded, not boxed, so it mitres its corners on a run that isn't axis-aligned.
static void Brackets( ArchMesh canvas, ArchRunPath run, ArchProfile gutter, ArchKit kit, ArchBrush brush )
{
var strap = kit.FindProfile( "gutter_bracket" );
var spacing = kit.GutterBracketSpacing;
if ( strap is null || !strap.IsUsable || spacing < 8f )
{
return;
}
// Bitten into the channel - short of it reads as a floating tab, exact touch is coplanar.
var bite = MathF.Max( strap.Max.x, strap.Max.y ) - 0.3f;
var front = gutter.Min.x - bite;
var under = gutter.Min.y - bite;
for ( var edge = 0; edge < run.Edges; edge++ )
{
var start = run.At( edge );
var end = run.At( edge + 1 );
var length = (end - start).Length;
if ( length < 16f )
{
continue;
}
var along = (end - start) / length;
var forward = new Vector3( along.x, along.y, 0f );
var right = Vector3.Cross( Vector3.Up, forward ).Normal;
var orientation = Rotation.LookAt( Vector3.Up, forward );
var origin = new Vector3( start.x, start.y, run.Height );
var inset = MathF.Min( 8f, length * 0.25f );
foreach ( var offset in ArchDivide.AtMost( length - inset * 2f, spacing ).Nodes )
{
var at = origin + forward * (inset + offset);
var path = new List<Vector3>
{
at + Vector3.Up * under,
at + right * front + Vector3.Up * under,
at + right * front + Vector3.Up * -0.5f
};
canvas.Extrude( path, strap, 1f, orientation, brush );
}
}
}
// Snaps to the nearest run, so a click on a gable end lands on a real eave.
public static ArchDownpipePart Place( ArchPlan plan, ArchBuilding building, ArchKit kit, Vector2 point, string profileName, bool shoe, PipeRun run = PipeRun.Rainwater )
{
var gutter = kit.FindProfile( "gutter" );
var pipe = kit.FindProfile( profileName );
if ( building is null || gutter is null || pipe is null || !pipe.IsUsable )
{
return null;
}
var kinds = ArchKinds.Load();
var best = float.MaxValue;
var tap = Vector3.Zero;
var outward = Vector2.Zero;
foreach ( var roof in plan.Filed<ArchRoofPart>( building, kinds ) )
{
foreach ( var eaveRun in ArchRoofGen.GutterRuns( roof, building, kit, plan ) )
{
if ( !eaveRun.Nearest( point, out var station, out var gap ) || gap >= best )
{
continue;
}
best = gap;
tap = station.Raised;
outward = station.Outward;
}
}
if ( best == float.MaxValue )
{
return null;
}
var eave = new Vector2( tap.x, tap.y );
var radius = MathF.Max( pipe.Max.x, -pipe.Min.x );
var face = WallFace( building, kit, eave, outward, out var normal );
var anchor = face + normal * (radius + kit.DownpipeWallGap);
var conduit = run == PipeRun.Conduit;
return new ArchDownpipePart
{
Id = plan.AllocateId(),
Name = $"{(conduit ? "Conduit" : "Downpipe")}{plan.CountFiled<ArchDownpipePart>( building, kinds ) + 1}",
// A conduit taps nothing, so it stands off the elevation the whole way and never swans over to it.
Outlet = conduit ? anchor : eave + outward * (MathF.Abs( gutter.Min.x ) * 0.5f),
Wall = conduit ? face : anchor,
TopHeight = tap.z + gutter.Min.y + 1.5f,
BaseHeight = Ground( building ),
Profile = profileName,
Shoe = shoe && !conduit,
Run = run
};
}
// The elevation's own face, plus the way out of it: a collar hangs a radius clear of it, a strap spans to it.
static Vector2 WallFace( ArchBuilding building, ArchKit kit, Vector2 eave, Vector2 outward, out Vector2 normal )
{
ArchWall best = null;
var closest = eave;
var bestDistance = float.MaxValue;
normal = outward;
foreach ( var room in building.Rooms )
{
var wall = ArchTool.NearestWall( room, eave, out var along, out var distance );
if ( wall is null || distance >= bestDistance )
{
continue;
}
bestDistance = distance;
best = wall;
closest = wall.PointAt( along );
}
if ( best is null )
{
return eave;
}
normal = best.Normal;
if ( Vector2.Dot( normal, outward ) < 0f )
{
normal = -normal;
}
var thickness = best.Thickness > 0f ? best.Thickness : kit.WallThickness;
return closest + normal * (thickness * 0.5f);
}
static float Ground( ArchBuilding building )
{
var lowest = float.MaxValue;
foreach ( var room in building.Rooms )
{
lowest = MathF.Min( lowest, room.BaseHeight );
}
return lowest == float.MaxValue ? 0f : lowest;
}
const float FloorClear = 2f;
const float ShoeRise = 7f;
const float StubDrop = 3f;
const float ShortestNeck = 12f;
// One mitred polyline, so the extruder makes the elbows.
public static void Downpipe( ArchMesh canvas, ArchDownpipePart pipe, ArchKit kit, ArchBrush brush )
{
var profile = kit.FindProfile( pipe.Profile );
if ( profile is null || !profile.IsUsable )
{
return;
}
var reach = pipe.Outlet - pipe.Wall;
var stand = reach.Length;
var outward = stand < 0.01f ? Vector2.Zero : reach / stand;
var floor = pipe.BaseHeight + FloorClear;
var bottom = Foot( pipe );
var stub = Head( pipe );
var neck = stub - stand;
var swans = !pipe.IsConduit && stand > 0.5f && neck > bottom + ShortestNeck;
// Too little wall to swan-neck into, so it drops where it taps.
var foot = swans ? pipe.Wall : pipe.Outlet;
var path = new List<Vector3> { At( pipe.Outlet, pipe.TopHeight ) };
if ( swans )
{
path.Add( At( pipe.Outlet, stub ) );
path.Add( At( pipe.Wall, neck ) );
}
path.Add( At( foot, bottom ) );
if ( pipe.Shoe )
{
path.Add( At( foot + outward * 5f, floor ) );
}
canvas.Extrude( path, profile, 1f, Rotation.Identity, brush );
if ( !pipe.IsConduit )
{
Outlet( canvas, pipe, profile, brush );
}
Fixings( canvas, pipe, foot, profile, kit, brush );
}
// Where every collar or bracket down a run stands, so the generator and the report count the same fixings.
public static List<float> Fixings( ArchDownpipePart pipe, ArchKit kit )
{
var from = Foot( pipe );
var run = Head( pipe ) - from;
var pitch = pipe.IsConduit ? kit.ConduitBracketSpacing : kit.DownpipeBracketSpacing;
var stations = new List<float>();
if ( pitch < 8f || run < 16f )
{
return stations;
}
foreach ( var offset in ArchDivide.AtMost( run, pitch ).Inner )
{
stations.Add( from + offset );
}
return stations;
}
static float Foot( ArchDownpipePart pipe ) => pipe.BaseHeight + FloorClear + (pipe.Shoe ? ShoeRise : 0f);
static float Head( ArchDownpipePart pipe ) => pipe.TopHeight - StubDrop;
static void Outlet( ArchMesh canvas, ArchDownpipePart pipe, ArchProfile profile, ArchBrush brush )
{
var path = new List<Vector3>
{
At( pipe.Outlet, pipe.TopHeight + 1f ),
At( pipe.Outlet, pipe.TopHeight - StubDrop )
};
canvas.Extrude( path, profile, 1.35f, Rotation.Identity, brush );
}
// A rainwater pipe wears collars round itself; a conduit is strapped off the wall face it runs down.
static void Fixings( ArchMesh canvas, ArchDownpipePart pipe, Vector2 foot, ArchProfile profile, ArchKit kit, ArchBrush brush )
{
var stations = Fixings( pipe, kit );
if ( stations.Count == 0 )
{
return;
}
if ( !pipe.IsConduit )
{
foreach ( var height in stations )
{
canvas.Extrude( new List<Vector3> { At( foot, height - 1.2f ), At( foot, height + 1.2f ) },
profile, 1.3f, Rotation.Identity, brush );
}
return;
}
var strap = kit.FindProfile( "gutter_bracket" );
if ( strap is null || !strap.IsUsable || (pipe.Wall - foot).Length < 0.5f )
{
return;
}
using ( canvas.Part( ArchPieces.Brackets ) )
{
foreach ( var height in stations )
{
canvas.Extrude( new List<Vector3> { At( foot, height ), At( pipe.Wall, height ) },
strap, 1f, Rotation.Identity, brush );
}
}
}
static Vector3 At( Vector2 point, float height ) => new( point.x, point.y, height );
}