Editor/Wall/ArchOpeningRunAffector.cs
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;
}
// Slotted by the wall and the ordinal on it, so a bay nudged along the same wall re-dresses the units it
// already owns there rather than closing them and allocating new ids for the same rhythm.
static bool Restand( ArchPlan plan, ArchKit kit, ArchRoom room, ArchWall wall ) {
var preset = Named( kit, wall.OpeningRun );
var wanted = Wanted( kit, room, wall, preset );
return ArchOwned.Settle( plan, ArchOwned.Openings, wall.Id,
wanted.Select( unit => Unit( wall, preset, unit ) ).ToList(), () => new ArchOpening() );
}
// Dressed off the preset in full, not just where it sits: retyping the rhythm's preset has to reach a unit
// that is already standing, and every field but identity comes from the preset.
static ArchWant<ArchWall, ArchOpening> Unit( ArchWall wall, ArchOpeningPreset preset, ArchOpeningShape unit ) {
return new ArchWant<ArchWall, ArchOpening>( wall, opening => {
preset.Dress( opening, unit.Offset );
opening.Width = unit.Width;
opening.Height = unit.Height;
opening.SillHeight = unit.SillHeight;
} );
}
// 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 );
}
// 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 );
}
}