Editor utility that generates roof framing geometry for architectural roofs. It computes rectangular blocks from footprint cells, builds roof cross-section knots for different roof styles, and adds rafters and purlins to an ArchMesh using spacing, sizes from an ArchKit, and an ArchBrush.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Rafters and purlins both cut from one cross-section profile, so the frame can't disagree with the deck.
public static partial class ArchRoofGen
{
static void Frame( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> region, ArchKit kit, ArchBrush brush )
{
if ( roof.Style is RoofStyle.Hip or RoofStyle.Sawtooth )
{
Log.Info( $"Architecture: {roof.Name} is a {roof.Style}, which sheds over its own skeleton rather than one span, so it grows no exposed frame." );
return;
}
var alongX = roof.RidgeAlongX;
var rafter = MathF.Max( 2f, kit.RoofRafterDepth );
var spacing = roof.FrameSpacing > 4f ? roof.FrameSpacing : MathF.Max( 12f, kit.RoofRafterSpacing );
var half = MathF.Max( 0.5f, kit.RoofRafterWidth ) * 0.5f;
// One frame per rectangular block, so an L's rafters stop at its notch and each wing rides its own ridge.
foreach ( var block in Blocks( region, alongX ) )
{
var runFrom = alongX ? block.Min.x : block.Min.y;
var runTo = alongX ? block.Max.x : block.Max.y;
var knots = Section( roof, alongX ? block.Min.y : block.Min.x, alongX ? block.Max.y : block.Max.x );
if ( runTo - runFrom < 1f || knots.Count < 2 )
{
continue;
}
Rafters( canvas, knots, alongX, runFrom, runTo, spacing, rafter, half, brush );
Purlins( canvas, knots, alongX, runFrom, runTo, kit, rafter, brush );
}
}
// Rectangular sub-blocks of a rectilinear region: the cell grid's columns merged across, then columns whose
// across run continues merged along.
static IEnumerable<ArchBox> Blocks( IReadOnlyList<List<Vector2>> loops, bool alongX )
{
var strips = new List<(float From, float To, float Low, float High)>();
foreach ( var column in ArchFootprint.Cells( loops, null )
.GroupBy( cell => alongX ? (cell.Min.x, cell.Max.x) : (cell.Min.y, cell.Max.y) )
.OrderBy( group => group.Key.Item1 ) )
{
var slices = column
.Select( cell => alongX ? (cell.Min.y, cell.Max.y) : (cell.Min.x, cell.Max.x) )
.OrderBy( slice => slice.Item1 );
float? low = null;
var high = 0f;
foreach ( var (from, to) in slices )
{
if ( low is null )
{
low = from;
high = to;
continue;
}
if ( from > high + 0.05f )
{
strips.Add( (column.Key.Item1, column.Key.Item2, low.Value, high) );
low = from;
}
high = MathF.Max( high, to );
}
if ( low is not null )
{
strips.Add( (column.Key.Item1, column.Key.Item2, low.Value, high) );
}
}
var merged = new List<(float From, float To, float Low, float High)>();
foreach ( var strip in strips.OrderBy( strip => strip.Low ).ThenBy( strip => strip.From ) )
{
var index = merged.FindIndex( held => MathF.Abs( held.Low - strip.Low ) < 0.05f
&& MathF.Abs( held.High - strip.High ) < 0.05f
&& MathF.Abs( held.To - strip.From ) < 0.05f );
if ( index >= 0 )
{
merged[index] = (merged[index].From, strip.To, strip.Low, strip.High);
continue;
}
merged.Add( strip );
}
return merged.Select( strip => alongX
? new ArchBox { Min = new Vector2( strip.From, strip.Low ), Max = new Vector2( strip.To, strip.High ) }
: new ArchBox { Min = new Vector2( strip.Low, strip.From ), Max = new Vector2( strip.High, strip.To ) } );
}
// Everything here reads the roof through these knots - each style described once.
static List<Vector2> Section( ArchRoofPart roof, float from, float to )
{
var span = to - from;
if ( roof.Style == RoofStyle.Shed )
{
var rise = ArchRoofPlane.Rise( roof, span );
return roof.Reversed
? new List<Vector2> { new( from, roof.BaseHeight + rise ), new( to, roof.BaseHeight ) }
: new List<Vector2> { new( from, roof.BaseHeight ), new( to, roof.BaseHeight + rise ) };
}
if ( roof.Style == RoofStyle.Gable )
{
return new List<Vector2>
{
new( from, roof.BaseHeight ),
new( (from + to) * 0.5f, roof.BaseHeight + ArchRoofPlane.Rise( roof, span * 0.5f ) ),
new( to, roof.BaseHeight )
};
}
return new List<Vector2> { new( from, roof.BaseHeight ), new( to, roof.BaseHeight ) };
}
// Segmented per knot pair so a gable's slopes meet at the ridge instead of cutting it.
static void Rafters( ArchMesh canvas, IReadOnlyList<Vector2> knots, bool alongX, float from, float to, float spacing, float depth, float half, ArchBrush brush )
{
foreach ( var station in ArchDivide.AtMost( to - from, spacing ).Nodes )
{
var run = from + station;
for ( var index = 0; index < knots.Count - 1; index++ )
{
var near = knots[index];
var far = knots[index + 1];
canvas.Rake(
At( alongX,run, near.x ), At( alongX,run, far.x ),
-half, half, near.y - depth, far.y - depth, near.y, far.y, brush );
}
}
}
// Purlin heights are fixed by cross position; only stations cut per slope, or a division straddles the ridge.
static void Purlins( ArchMesh canvas, IReadOnlyList<Vector2> knots, bool alongX, float from, float to, ArchKit kit, float rafter, ArchBrush brush )
{
var depth = MathF.Max( 2f, kit.RoofPurlinDepth );
var half = MathF.Max( 0.5f, kit.RoofPurlinWidth ) * 0.5f;
var spacing = MathF.Max( 12f, kit.RoofPurlinSpacing );
for ( var index = 0; index < knots.Count - 1; index++ )
{
var near = knots[index];
var far = knots[index + 1];
var reach = far.x - near.x;
if ( reach < 1f )
{
continue;
}
foreach ( var station in ArchDivide.AtMost( reach, spacing ).Nodes )
{
var cross = near.x + station;
var top = near.y + (far.y - near.y) * (station / reach) - rafter;
canvas.Beam( At( alongX,from, cross ), At( alongX,to, cross ), -half, half, top - depth, top, brush );
}
}
}
static Vector2 At( bool alongX, float run, float cross )
{
return alongX ? new Vector2( run, cross ) : new Vector2( cross, run );
}
}