Editor utility that computes and applies a run of architectural openings (doors/windows) along a wall based on a kit and a preset. It determines what openings should stand, compares against current owned openings, closes outdated ones, and spawns new openings with resolved sizes and offsets.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A bay rhythm is one setting on the wall, not forty records - but the units it stands ARE records, because a
// wall's hole is an authored ArchOpening and nothing else wears a frame, a sash and a sill.
//
// So they are an effect, and effects obey the contract: re-derived from the rhythm as it stands NOW on every
// commit, and closed the moment it stops standing. The wall owns them, so retyping the preset, dragging an end
// or changing the bay all hand back exactly what the rhythm currently describes.
public static class ArchOpeningRunAffector
{
// The masonry between two units, and between a unit and something placed by hand. Below this the rhythm
// is a strip window, which is a preset rather than a rhythm.
const float LeastPier = 8f;
public static int Resolve( ArchPlan plan, ArchKit kit )
{
var kinds = ArchKinds.Load();
var stood = 0;
foreach ( var room in plan.AllRooms().ToList() )
{
foreach ( var wall in plan.Filed<ArchWall>( room, kinds ).ToList() )
{
if ( Restand( plan, kit, room, wall ) )
{
stood++;
}
}
}
return stood;
}
// Compared by where each unit SITS, not by how many there are - a bay nudged along the same wall wants the
// same count and different offsets, and a count check would skip the re-stand.
static bool Restand( ArchPlan plan, ArchKit kit, ArchRoom room, ArchWall wall )
{
var preset = Named( kit, wall.OpeningRun );
var wanted = Wanted( kit, room, wall, preset );
var fitted = preset?.Furniture ?? OpeningFurniture.None;
if ( Standing( wall ).SetEquals( wanted.Select( unit => Signature( wall.OpeningRun.Preset, fitted, unit ) ) ) )
{
return false;
}
Close( wall );
foreach ( var unit in wanted )
{
var opening = preset.Spawn( plan.AllocateId(), unit.Offset );
opening.OwnerId = wall.Id;
opening.Width = unit.Width;
opening.Height = unit.Height;
opening.SillHeight = unit.SillHeight;
wall.Openings.Add( opening );
}
return true;
}
// Exact, never FindOpening's fallback: a rhythm naming a preset the kit has lost would quietly stand
// twelve of whatever happened to be first.
static ArchOpeningPreset Named( ArchKit kit, ArchOpeningRun run )
{
return run.Stands
? kit.Openings.FirstOrDefault( preset => string.Equals( preset.Name, run.Preset, StringComparison.OrdinalIgnoreCase ) )
: null;
}
// Stations come off ArchDivide over the wall's USABLE run, so the end units sit inside the margin instead of
// stacking on it, and each one resolves through the placement service a drag uses.
static List<ArchOpeningShape> Wanted( ArchKit kit, ArchRoom room, ArchWall wall, ArchOpeningPreset preset )
{
var wanted = new List<ArchOpeningShape>();
if ( preset is null || wall.Length < 1f )
{
return wanted;
}
var run = wall.OpeningRun;
ArchOpeningSeats.Usable( wall, preset, kit, true, ArchGridService.FinestSize, out var runFrom, out var runTo );
var usable = runTo - runFrom;
var width = ArchGridService.Fine( run.Width > 1f ? run.Width : preset.Width );
var fits = (int)MathF.Floor( usable / MathF.Max( 1f, width + LeastPier ) );
// Asking for more than the elevation holds is answered with as many as it holds - openings that
// overlap are one broken panel, not a tighter rhythm.
if ( fits < 1 )
{
return wanted;
}
var asked = run.Count > 0 ? run.Count : ArchDivide.AtMost( usable, run.Bay ).Count;
var division = ArchDivide.Into( usable, Math.Min( asked, fits ) );
var wallHeight = ArchWallSection.Height( wall, room, kit );
foreach ( var centre in division.Centres )
{
var unit = ArchOpeningSeats.Stationed( wall, preset, kit, run, runFrom + centre, wallHeight );
if ( Clear( wall, unit ) )
{
wanted.Add( unit );
}
}
return wanted;
}
// A hand-placed unit keeps its place and the rhythm parts around it. That is what an individual override is
// here: drag the door you want and the windows make room for it, rather than editing one of twelve clones.
static bool Clear( ArchWall wall, ArchOpeningShape unit )
{
var left = unit.Offset - unit.Width * 0.5f;
var right = unit.Offset + unit.Width * 0.5f;
return !wall.Openings
.Where( opening => opening.OwnerId != wall.Id )
.Any( opening => right > opening.Left - LeastPier && left < opening.Right + LeastPier );
}
static HashSet<string> Standing( ArchWall wall )
{
return wall.Openings
.Where( opening => opening.OwnerId == wall.Id )
.Select( opening => Signature( opening.Preset, opening.Furniture, opening.Offset, opening.Width, opening.SillHeight, opening.Height ) )
.ToHashSet();
}
static string Signature( string preset, OpeningFurniture furniture, ArchOpeningShape unit )
{
return Signature( preset, furniture, unit.Offset, unit.Width, unit.SillHeight, unit.Height );
}
static string Signature( string preset, OpeningFurniture furniture, float offset, float width, float sill, float height )
{
return $"{preset}:{furniture}:{offset:0.#}:{width:0.#}:{sill:0.#}:{height:0.#}";
}
// The other half of being able to re-stand. Only the rhythm's own units - a hand-placed one is nobody
// else's to close.
public static void Close( ArchWall wall )
{
wall.Openings.RemoveAll( opening => opening.OwnerId == wall.Id );
}
}