Editor code that generates and manages roof parapet walls for the architecture system. It computes a common seat height for roof walls, creates a scoped deck host (room and connection service) for roof walls, files walls onto a plan, and converts a uniform parapet ring into individual walls with parapet-specific defaults.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// The deck a roof wall is generated against, resolved once so the room and the joins can never disagree about
// which walls are up there or how high they stand.
public readonly struct ArchRoofDeckHost
{
public ArchRoom Room { get; init; }
public ArchConnectionService Connections { get; init; }
public float Seat => Room.BaseHeight;
}
// A WALL STANDING ON A DECK: a parapet that varies edge by edge, a party wall between two rooftops, a screen
// round plant. It is an ordinary ArchWall, so it wears every treatment, band, coping, rhythm and opening a wall
// on a storey wears - it is merely filed on the roof, because no room stands up there to own it.
//
// roof.Parapet is still the quick uniform ring. StandParapet turns that ring into walls, which is the only way a
// height, a treatment or a gap can differ from one edge to the next - the same idiom as breaking an opening
// rhythm by dragging one of its units.
public static class ArchRoofWalls
{
public const float ParapetHeight = 18f;
public static bool Stands( ArchRoofPart roof ) => ArchPlanStore.FiledOn( ArchKind.Wall, roof ).Any();
// THE seat every wall on this roof stands on, read by the generator, the ghost and the report alike: the
// LOWEST deck surface under the section, so a wall on a rake buries its uphill end instead of floating its
// downhill one. ONE height for the whole roof, because two walls meeting at a corner have to arrive there at
// the same height or neither mitres.
public static float Seat( ArchRoofPart roof, ArchKit kit )
{
var lowest = float.MaxValue;
foreach ( var corner in roof.Outline() )
{
lowest = MathF.Min( lowest, ArchAsks.RoofSeat( roof, kit, corner ) );
}
return lowest < float.MaxValue ? lowest : roof.BaseHeight;
}
// The one place a stand-in room is made, and it is scoped to the deck alone: a storey above the roof's own
// level so nothing in the building under it reads as covering these walls, and a connection service holding
// only this room so the roof's walls mitre with each other and with nothing downstairs.
public static ArchRoofDeckHost Deck( ArchRoofPart roof, ArchKit kit )
{
var room = new ArchRoom
{
Id = roof.Id,
Name = roof.Name,
Floor = roof.Level + 1,
BaseHeight = Seat( roof, kit ),
WallHeight = ParapetHeight,
HasFloor = false,
HasCeiling = false,
RaisedFoundation = false,
Palette = roof.Palette,
Walls = ArchPlanStore.FiledOn( ArchKind.Wall, roof ).OfType<ArchWall>().ToList()
};
var deck = new ArchBuilding { Id = roof.Id, Name = roof.Name, Rooms = new List<ArchRoom> { room } };
return new ArchRoofDeckHost { Room = room, Connections = new ArchConnectionService( deck, kit ) };
}
// Whatever style: a parapet is a wall standing at a seat, and every deck has one. Only what is CUT OUT of a deck
// needs the axis-aligned patches ArchRoofPlane.Planar gates on.
public static ArchRoofPart Under( ArchPlan plan, int level, Vector2 point ) => ArchRoofLight.Over( plan, level, point );
public static ArchWall Add( ArchPlan plan, ArchRoofPart roof, Vector2 from, Vector2 to, ArchWallPreset treatment )
{
if ( roof is null || (to - from).Length < 1f )
{
return null;
}
var wall = Parapeted( treatment.Spawn( plan.AllocateId(), from, to ) );
plan.File( ArchKind.Wall, roof, wall );
return wall;
}
// The ring stood as real walls, one per edge of the deck, so what was one number becomes a wall each author
// can raise, drop, dress or delete on its own. The uniform ring is switched off in the same breath, or the
// deck carries both at once.
public static IReadOnlyList<ArchWall> StandParapet( ArchPlan plan, ArchRoofPart roof, ArchKit kit, ArchWallPreset treatment )
{
var outline = ArchFootprint.Wind( roof.Outline() );
var height = roof.Parapet ? MathF.Max( 4f, roof.ParapetHeight ) : ParapetHeight;
var stood = new List<ArchWall>();
var kinds = ArchKinds.Load();
for ( var index = 0; index < outline.Count; index++ )
{
var from = outline[index];
var to = outline[(index + 1) % outline.Count];
if ( (to - from).Length < 1f )
{
continue;
}
var wall = Parapeted( treatment.Spawn( plan.AllocateId(), from, to ) );
wall.Height = height;
wall.Thickness = MathF.Max( 2f, kit.WallThickness );
plan.File( ArchKind.Wall, roof, wall, kinds );
stood.Add( wall );
}
roof.Parapet = false;
return stood;
}
// What a wall standing on a deck IS, as opposed to one standing in a room: both faces are weather, its head
// is finished by a coping rather than run under an eave, and there is no floor for a skirting to die into.
static ArchWall Parapeted( ArchWall wall )
{
wall.Height = wall.Height > 0.5f ? wall.Height : ParapetHeight;
wall.Exterior = true;
wall.Baseboard = false;
wall.Cap = true;
return wall;
}
}