Editor/Porch/ArchPorchAffector.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A porch is an ENTRANCE, and an entrance with no door in the wall behind it is a deck you cannot get off.
// The door was a separate drag you had to remember, aimed past the porch geometry now standing over the wall
// it belongs in - so the porch stands it, and the porch owns it.
//
// It lands on a host the porch does not own - the wall - so it is an affector and obeys the contract: the
// elevation it fronts is re-derived from where the legs stand NOW on every commit, so reshaping the deck moves
// the door with it, and turning the option off closes the hole rather than orphaning it.
//
// Owned by the PORCH's id, not the wall's, which is what keeps it out of ArchOpeningRunAffector's hands: that
// pass closes only its own units, and its Clear test reads anything owned by somebody else as hand-placed and
// parts the rhythm around it. A porch door and a bay rhythm on one elevation therefore need no code between them.
public static class ArchPorchAffector {
// Below this overlap the deck is beside the elevation rather than fronting it, and a door would be squeezed
// into masonry that has to hold the wall up.
const float LeastFrontage = 36f;
const float Tolerance = 12f;
public static int Resolve( ArchPlan plan, ArchKit kit ) {
var stood = 0;
var kinds = ArchKinds.Load();
foreach ( var building in plan.Buildings.ToList() ) {
foreach ( var room in building.Rooms.ToList() ) {
foreach ( var porch in plan.Filed<ArchPorchPart>( room, kinds ).ToList() ) {
// The section over it and the flights off it land on hosts the porch does not own - the
// building's roof list, and grade - so both are re-derived from where the legs stand NOW,
// exactly as the door is. The cover goes first: the head every flight climbs to is the one
// the roof was just seated on.
var recovered = ArchPorchCover.Resolve( plan, building, room, kit, porch );
ArchPorchSteps.Settle( porch, ArchPorchShape.Resolve( porch, room, building, kit ), kit );
if ( Restand( plan, kit, building, room, porch ) || recovered ) {
stood++;
}
}
}
}
return stood;
}
// One slot or none: the door option turned off wants nothing, and wanting nothing is what reaps the hole
// rather than orphaning it in the elevation the deck has stopped fronting.
static bool Restand( ArchPlan plan, ArchKit kit, ArchBuilding building, ArchRoom room, ArchPorchPart porch ) {
var wants = new List<ArchWant<ArchWall, ArchOpening>>();
if ( porch.Door && Wanted( kit, building, room, porch ) is { } door ) {
wants.Add( Hung( door ) );
}
return ArchOwned.Settle( plan, ArchOwned.Openings, porch.Id, wants, () => new ArchOpening() );
}
static ArchWant<ArchWall, ArchOpening> Hung( Door door ) {
return new ArchWant<ArchWall, ArchOpening>( door.Wall, opening => {
door.Preset.Dress( opening, door.Shape.Offset );
opening.Width = door.Shape.Width;
opening.Height = door.Shape.Height;
opening.SillHeight = door.Shape.SillHeight;
} );
}
readonly record struct Door( ArchWall Wall, ArchOpeningPreset Preset, ArchOpeningShape Shape );
// The elevation the deck fronts WIDEST, over every leg and every wall on the storey - a wrap-around turns a
// corner, and the door belongs on the side it fronts most of, not on whichever wall a leg happened to list first.
static Door? Wanted( ArchKit kit, ArchBuilding building, ArchRoom room, ArchPorchPart porch ) {
if ( Leaf( kit, porch ) is not { } preset ) {
return null;
}
ArchWall best = null;
ArchRoom host = null;
var widest = LeastFrontage;
var station = 0f;
foreach ( var storey in building.Rooms.Where( entry => entry.Floor == room.Floor ) ) {
foreach ( var wall in storey.Walls ) {
foreach ( var leg in porch.Legs ) {
if ( !Fronts( wall, leg, ArchWallSection.Thickness( wall, kit ), out var from, out var to ) || to - from <= widest ) {
continue;
}
best = wall;
host = storey;
widest = to - from;
station = (from + to) * 0.5f;
}
}
}
if ( best is null || Already( best, porch, station, widest ) ) {
return null;
}
return new Door( best, preset,
ArchOpeningSeats.Stationed( best, preset, kit, new ArchOpeningRun(), station,
ArchWallSection.Height( best, host, kit ) ) );
}
// A hand-placed door in the stretch the deck fronts IS the way in, so the porch stands nothing beside it.
// Without this, switching the option on - and it defaults on, so every porch already authored - punched a
// second door alongside the one the author had already drawn.
static bool Already( ArchWall wall, ArchPorchPart porch, float station, float frontage ) {
var from = station - frontage * 0.5f;
var to = station + frontage * 0.5f;
return wall.Openings.Any( opening =>
opening.OwnerId != porch.Id && opening.Kind.Hangs()
&& opening.Right > from && opening.Left < to );
}
// How much of this wall's run the leg stands across, and whether it is against its face at all. A deck lying
// outside the elevation touches the outer face; one straddling the centreline is a leg drawn over the wall.
static bool Fronts( ArchWall wall, ArchPorchLeg leg, float thickness, out float from, out float to ) {
from = 0f;
to = 0f;
var length = wall.Length;
if ( length < 1f ) {
return false;
}
var corners = new[] { leg.Min, new Vector2( leg.Max.x, leg.Min.y ), leg.Max, new Vector2( leg.Min.x, leg.Max.y ) };
var runs = corners.Select( corner => Vector2.Dot( corner - wall.Start, wall.Direction ) ).ToList();
var sides = corners.Select( corner => Vector2.Dot( corner - wall.Start, wall.Normal ) ).ToList();
from = Math.Clamp( runs.Min(), 0f, length );
to = Math.Clamp( runs.Max(), 0f, length );
var near = sides.Min() < 0f && sides.Max() > 0f
? 0f
: MathF.Min( MathF.Abs( sides.Min() ), MathF.Abs( sides.Max() ) );
return near <= thickness * 0.5f + Tolerance;
}
// Named, else the kit's first door. A leaf is Door/DoubleDoor/Garage only, and ArchOpeningKinds is the one
// place that is asked - a porch fronted by an archway would carry nothing to open.
static ArchOpeningPreset Leaf( ArchKit kit, ArchPorchPart porch ) {
if ( porch.DoorPreset is { Length: > 0 } named ) {
return kit.Openings.FirstOrDefault( preset =>
string.Equals( preset.Name, named, StringComparison.OrdinalIgnoreCase ) && preset.Kind.Hangs() );
}
return kit.Openings.FirstOrDefault( preset => preset.Kind == OpeningKind.Door );
}
// Every wall the building holds, not just the one the deck fronts now, or a reshaped porch leaves its old
// hole standing in the old elevation.
public static void Close( ArchBuilding building, ArchPorchPart porch ) {
foreach ( var wall in building.Rooms.SelectMany( room => room.Walls ) ) {
wall.Openings.RemoveAll( opening => opening.OwnerId == porch.Id );
}
}
}