Editor/Roof/ArchGutterGen.cs
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 ) {
normal = outward;
var best = ArchTool.NearestWall( building.Rooms, eave, out _, out var along, out _ );
if ( best is null ) {
return eave;
}
var closest = best.PointAt( along );
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 );
}