Editor/Porch/ArchPorchCover.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Filed on the building (where the roof generator reads it), nested under the porch by a layer record.
public static class ArchPorchCover {
const float DefaultPitch = 12f;
const float LeastHead = 78f;
public static float Pitched( ArchPorchPart porch ) => porch.Pitch > 0.5f ? porch.Pitch : DefaultPitch;
// Clamped to fit under the eave above — not written back, so a porch under a raised eave can grow.
public static float Headroom( ArchPorchPart porch, ArchBuilding building, ArchKit kit, float depth ) {
var wanted = MathF.Max( 60f, porch.HeadHeight );
if ( building is null ) {
return wanted;
}
ArchPorch.Bounds( porch, out var min, out var max );
var limit = float.MaxValue;
foreach ( var above in building.Roofs ) {
if ( above.BaseHeight > porch.BaseHeight && !IsPorchRoof( building, above ) && Overlaps( above, min, max ) ) {
limit = MathF.Min( limit, Underside( above, kit ) );
}
}
if ( limit == float.MaxValue ) {
return wanted;
}
// Rise is over the deck's depth from the wall, not the leg's length.
var rise = MathF.Tan( Pitched( porch ).DegreeToRadian() ) * (depth + kit.RoofOverhang * 0.75f);
var stack = rise + kit.RoofThickness + kit.PorchFlashingHeight + MathF.Max( 3f, kit.PorchBeamDepth ) + 2f;
return ArchClearance.Fits( wanted, limit - porch.BaseHeight - stack, LeastHead,
"porch head height", "its roof has to tuck under the eave above it" );
}
// A real hip sheds the L, so a wrap-around needs no second roof.
public static bool Resolve( ArchPlan plan, ArchBuilding building, ArchRoom room, ArchKit kit, ArchPorchPart porch ) {
var standing = building.Roofs.FirstOrDefault( part => part.Id == porch.RoofId );
var footprint = ArchPorch.Footprint( porch );
var wanted = porch.Roofed && footprint.Count >= 4;
if ( !wanted ) {
if ( standing is null ) {
return false;
}
Remove( plan, building, porch );
return true;
}
var shape = ArchPorchShape.Resolve( porch, room, building, kit );
var cover = standing;
if ( cover is null ) {
cover = new ArchRoofPart {
Id = plan.AllocateId(),
Name = $"{porch.Name} Roof",
Level = room.Floor,
Style = RoofStyle.Hip,
Overhang = kit.RoofOverhang * 0.75f,
Thickness = kit.RoofThickness,
Gutters = true,
Fascia = true,
Soffit = true,
EndWalls = false,
Ceiling = false
};
plan.File( ArchKind.Roof, building, cover );
porch.RoofId = cover.Id;
}
var before = $"{cover.Pitch:0.##}:{cover.BaseHeight:0.##}:{cover.Min}:{cover.Max}";
cover.Pitch = Pitched( porch );
cover.Reshape( footprint );
cover.BaseHeight = shape.Head + kit.PorchBeamDepth;
Nest( plan, cover, porch );
return standing is null || before != $"{cover.Pitch:0.##}:{cover.BaseHeight:0.##}:{cover.Min}:{cover.Max}";
}
// The section is the building's, which is where the generator wants it. A layer record moves only its
// ROW, so the stack shows it as part of the porch it covers.
static void Nest( ArchPlan plan, ArchRoofPart cover, ArchPorchPart porch ) {
ArchLayerGroups.Leave( plan, cover.Id );
var record = plan.Layers.FirstOrDefault( entry => entry.ItemId == cover.Id );
if ( record is null ) {
record = new ArchLayerRecord { ItemId = cover.Id, Kind = ArchKind.Roof, Stage = ArchLayerStage.Structure };
plan.Layers.Add( record );
}
record.ParentId = porch.Id;
}
public static void Remove( ArchPlan plan, ArchBuilding building, ArchPorchPart porch ) {
if ( building is null || porch.RoofId == 0 ) {
return;
}
plan.RemoveAll( ArchKind.Roof, building, roof => ArchPlanStore.IdOf( roof ) == porch.RoofId );
plan?.Layers.RemoveAll( record => record.ItemId == porch.RoofId );
porch.RoofId = 0;
}
// A porch roof is not an eave to tuck under.
static bool IsPorchRoof( ArchBuilding building, ArchRoofPart roof ) {
var kinds = ArchKinds.Load();
return building.Rooms
.SelectMany( room => ArchPlanStore.FiledOn( ArchKind.Porch, room, kinds ) )
.OfType<ArchPorchPart>()
.Any( porch => porch.RoofId == roof.Id );
}
// The line to stay under is the gutter's bottom, not the eave itself.
static float Underside( ArchRoofPart roof, ArchKit kit ) {
var profile = roof.Gutters && !roof.Parapet ? kit.FindProfile( "gutter" ) : null;
if ( profile is null || !profile.IsUsable ) {
return roof.BaseHeight - kit.FasciaHeight;
}
return roof.BaseHeight - profile.Max.y + profile.Min.y;
}
static bool Overlaps( ArchRoofPart roof, Vector2 min, Vector2 max ) {
var reach = new Vector2( roof.Overhang, roof.Overhang );
return ArchFootprint.Overlaps(
new ArchBox { Min = min, Max = max },
new ArchBox { Min = roof.Min - reach, Max = roof.Max + reach } );
}
}