Editor/Wall/ArchStoreyCornice.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// The entablature a STOREY wears at its head. It is ONE ring round the storey's own exterior loop - chamfer facets
// and all - rather than a piece per wall, because a corner resolved by each of its two walls from its own join is a
// joinery case nobody owns. A wall still NAMES the style, and one that names none drops its segment out of the ring.
public static class ArchStoreyCornice {
public static void Build( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style ) {
if ( room is null || building is null || !Owns( room, building ) ) {
return;
}
// The loop is a whole footprint union - asked before the walls are, every bare storey in the plan pays for it.
var dressed = Dressed( room, building ).ToList();
if ( dressed.Count == 0 ) {
return;
}
var loop = Loop( room, building );
if ( loop is not { Count: >= 3 } ) {
return;
}
var head = Head( room, building, kit );
var chain = new[] { room.Palette, building.Palette };
var connections = new ArchConnections( building, kit );
foreach ( var named in dressed.GroupBy( wall => wall.Cornice ) ) {
if ( kit.FindCornice( named.Key ) is not { Stands: true } crown ) {
continue;
}
var laying = named.ToList();
new ArchCorniceRing {
Crown = crown,
Outline = loop,
Head = head,
Thickness = kit.WallThickness,
Abuts = ( point, _ ) => !Carries( laying, kit, point ),
Returns = Crossings( room, building, plan, kit, connections, crown, head ).ToList(),
Kit = kit,
Style = style,
Chain = chain,
Plan = plan,
Level = room.Floor,
HostId = building.Id,
Steps = false
}.Lay( canvas );
}
}
// Where a pier standing on this storey has to die: the ring's own soffit, so the two are resolved once and the
// pier head can never be driven up into the entablature.
public static float Soffit( IArchWallTreatment wall, ArchKit kit, float height ) {
return kit.FindCornice( wall.Cornice ) is { Stands: true } crown
? ArchCorniceRing.Soffit( crown, height, kit )
: height;
}
// One height for the whole storey, the way a deck's parapet seat is one height for the whole roof, or two walls
// meeting at a corner arrive at the ring from different heights and neither mitres.
public static float Head( ArchRoom room, ArchBuilding building, ArchKit kit ) {
var above = building.Rooms
.Where( other => other.Floor == room.Floor + 1 )
.Select( other => other.BaseHeight )
.DefaultIfEmpty( 0f )
.Min();
return above > room.BaseHeight + 0.5f ? above : room.BaseHeight + ArchWallSection.CeilingHeight( room, kit );
}
// The lowest-id room on the storey stands the ring for all of them - the same law that lets one wall build a
// shared boundary, or two rooms on one floor ring the same loop twice.
static bool Owns( ArchRoom room, ArchBuilding building ) {
return building.Rooms.Where( other => other.Floor == room.Floor ).OrderBy( other => other.Id ).FirstOrDefault() == room;
}
static List<Vector2> Loop( ArchRoom room, ArchBuilding building ) {
// Slanted loops kept: a chamfered storey is exactly the case the ring exists for.
var storey = ArchRegion.Footprints( building.Rooms.Where( other => other.Floor == room.Floor ), false );
return ArchFootprint.Outer( ArchFootprint.Union( storey ) ).FirstOrDefault();
}
static IEnumerable<ArchWall> Dressed( ArchRoom room, ArchBuilding building ) {
return building.Rooms
.Where( other => other.Floor == room.Floor )
.SelectMany( other => other.Walls.Where( wall => Standing( wall, other, building ) ) );
}
static bool Standing( ArchWall wall, ArchRoom room, ArchBuilding building ) {
return !string.IsNullOrWhiteSpace( wall.Cornice )
&& ArchLayerGate.On( wall )
&& !ArchWallJoins.CoveredBy( building, room, wall )
&& ArchWallFaces.TryOutward( wall, room, building, out _ );
}
// A segment belongs to the ring being laid when one of the walls that named this style covers it. Everything
// else - a party wall, a wall wearing another entablature, an interior run - is a break in the loop.
static bool Carries( IReadOnlyList<ArchWall> laying, ArchKit kit, Vector2 point ) {
foreach ( var wall in laying ) {
var span = wall.End - wall.Start;
var length = span.Length;
if ( length < 0.5f ) {
continue;
}
var along = span / length;
var travelled = Vector2.Dot( point - wall.Start, along );
if ( travelled < -0.5f || travelled > length + 0.5f ) {
continue;
}
if ( MathF.Abs( Vector2.Dot( point - wall.Start, wall.Normal ) ) <= ArchWallSection.Thickness( wall, kit ) ) {
return true;
}
}
return false;
}
// Everything standing proud of the storey's elevations, folded into the ONE list the ring steps its path out
// over. A pier whose cap dies under the soffit never reaches the band, so the ring runs straight over it.
static IEnumerable<ArchCorniceReturn> Crossings(
ArchRoom room,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
ArchConnections connections,
ArchCorniceStyle crown,
float head ) {
var foot = ArchCorniceRing.Soffit( crown, head, kit ) - room.BaseHeight;
var top = head - room.BaseHeight;
var reach = ArchCorniceGen.Reach( crown ) + ArchLap.Bite( kit );
foreach ( var other in building.Rooms.Where( entry => entry.Floor == room.Floor ) ) {
foreach ( var wall in other.Walls.Where( wall => ArchLayerGate.On( wall ) ) ) {
var thickness = ArchWallSection.Thickness( wall, kit );
var join = connections.JoinWall( wall, other, thickness );
foreach ( var shape in Proud( wall, other, building, plan, kit, join, top, thickness ) ) {
if ( !shape.Outside || shape.Carves || shape.Depth <= reach || !shape.Spans( foot, top, top ) ) {
continue;
}
yield return new ArchCorniceReturn(
wall.Start + wall.Direction * shape.From,
wall.Start + wall.Direction * shape.To,
shape.Depth );
}
}
}
}
static IEnumerable<ArchWallModShape> Proud(
ArchWall wall,
ArchRoom room,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
ArchWallJoin join,
float height,
float thickness ) {
var cuts = wall.Openings.Where( opening => ArchLayerGate.On( opening ) ).OrderBy( opening => opening.Left ).ToList();
var shapes = ArchWallModShape.For( wall, join, height, thickness, kit );
shapes.AddRange( ArchWallExtrude.Zones( plan, kit, building, room, wall, join.StartExtend, join.EndExtend, height, thickness, cuts ) );
shapes.AddRange( ArchWallPiers.Shapes( wall, join, kit, height, thickness, Soffit( wall, kit, height ) ) );
return shapes;
}
}