Editor/Wall/ArchTrimFollow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Which elements a following run is allowed to dress. A run round a shaft usually wants the floor edge and
// the jambs of every wall the shaft went through; one round a plinth wants the deck edge alone.
public enum ArchTrimReach {
Floor,
Walls,
Both
}
// ONE resolution of a following run's path. The generator, the ghost and the report all read this, so the
// run you are shown is the run you get.
//
// A ring on the floor and nothing on the wall it passes through is two mouldings that stop at each other.
// This walks the host's outline and, where an edge crosses a wall the host OPENED, turns the run up that
// opening's jamb, over its head and back down - so what comes out is one continuous path rather than a
// floor ring plus a set of loose uprights.
public static class ArchTrimFollow {
// Below this the jamb is a nick in the plaster, not a reveal worth a moulding round.
const float LeastJamb = 4f;
public static List<List<Vector3>> Runs( ArchPlan plan, ArchKit kit, ArchTrimPart trim ) {
var runs = new List<List<Vector3>>();
if ( trim is null ) {
return runs;
}
if ( !trim.Follows ) {
runs.Add( Closed( trim.Path, trim.Closed ) );
return runs;
}
var loops = ArchLayerShape.Loops( plan, trim.FollowsId );
if ( loops.Count == 0 ) {
return runs;
}
var loop = loops[Math.Clamp( trim.FollowsLoop, 0, loops.Count - 1 )];
var height = ArchLayerShape.HeightOf( plan, trim.FollowsId, 0f ) + trim.Lift;
var jambs = trim.Reach == ArchTrimReach.Floor
? new List<Jamb>()
: Jambs( plan, kit, trim.FollowsId ).ToList();
// Walls only: the uprights on their own, each its own open run - there is no floor edge to join them.
if ( trim.Reach == ArchTrimReach.Walls ) {
runs.AddRange( jambs.Select( jamb => Upright( jamb ) ) );
return runs;
}
runs.Add( Walked( loop, height, jambs ) );
return runs;
}
// The outline walked edge by edge, climbing every jamb standing on that edge in the order it is met, so
// the path never doubles back on itself.
static List<Vector3> Walked( IReadOnlyList<Vector2> loop, float height, List<Jamb> jambs ) {
var path = new List<Vector3>();
for ( var index = 0; index < loop.Count; index++ ) {
var a = loop[index];
var b = loop[(index + 1) % loop.Count];
path.Add( new Vector3( a.x, a.y, height ) );
foreach ( var jamb in Along( jambs, a, b ) ) {
path.AddRange( Upright( jamb ) );
}
}
// Closed: the last corner runs back to the first, which is what makes it ONE moulding.
if ( path.Count >= 3 ) {
path.Add( path[0] );
}
return path;
}
// A jamb visited as three points: up its face, over the head and down again, so the run reads as having
// gone round the reveal rather than jumped it.
static List<Vector3> Upright( Jamb jamb ) {
return new List<Vector3>
{
new( jamb.Foot.x, jamb.Foot.y, jamb.Bottom ),
new( jamb.Foot.x, jamb.Foot.y, jamb.Top ),
new( jamb.Head.x, jamb.Head.y, jamb.Top ),
new( jamb.Head.x, jamb.Head.y, jamb.Bottom )
};
}
// The jambs lying on this edge, ordered along it - two on the same edge have to be climbed in the order
// the walk reaches them or the moulding crosses itself.
static IEnumerable<Jamb> Along( List<Jamb> jambs, Vector2 a, Vector2 b ) {
var span = b - a;
var length = span.Length;
if ( length < 0.5f ) {
yield break;
}
var unit = span / length;
var found = jambs
.Select( jamb => (Jamb: jamb, At: Vector2.Dot( (jamb.Foot + jamb.Head) * 0.5f - a, unit )) )
.Where( entry => entry.At > 0.5f && entry.At < length - 0.5f )
.Where( entry => MathF.Abs( Across( (entry.Jamb.Foot + entry.Jamb.Head) * 0.5f - a, unit ) ) < LeastJamb )
.OrderBy( entry => entry.At );
foreach ( var entry in found ) {
yield return entry.Jamb;
}
}
static float Across( Vector2 offset, Vector2 unit ) => offset.x * unit.y - offset.y * unit.x;
// Every opening the host owns, as the reveal it left in its wall. Owned, so a run round a shaft climbs
// the holes that shaft made and never a door that happened to be beside it.
static IEnumerable<Jamb> Jambs( ArchPlan plan, ArchKit kit, int hostId ) {
var kinds = ArchKinds.Load();
foreach ( var room in plan.AllRooms() ) {
var wallHeight = ArchFloorGen.WallHeight( room, kit );
foreach ( var wall in plan.Filed<ArchWall>( room, kinds ) ) {
foreach ( var opening in wall.Openings.Where( entry => entry.OwnerId == hostId && ArchLayerGate.On( entry ) && ArchLayerGate.Owned( entry.OwnerId ) ) ) {
var height = MathF.Min( opening.Height, wallHeight - opening.SillHeight );
if ( height < LeastJamb || opening.Width < LeastJamb ) {
continue;
}
yield return new Jamb {
Foot = wall.PointAt( opening.Left ),
Head = wall.PointAt( opening.Right ),
Bottom = room.BaseHeight + opening.SillHeight,
Top = room.BaseHeight + opening.SillHeight + height
};
}
}
}
}
static List<Vector3> Closed( IReadOnlyList<Vector3> path, bool close ) {
var walked = new List<Vector3>( path );
if ( close && walked.Count >= 3 && (walked[0] - walked[^1]).Length > 0.5f ) {
walked.Add( walked[0] );
}
return walked;
}
// One reveal in one wall: where it starts, where it ends and the band it spans.
sealed class Jamb {
public Vector2 Foot;
public Vector2 Head;
public float Bottom;
public float Top;
}
}