Editor utility that computes moulding runs for architectural trims. It builds 3D paths that follow room outlines and climb openings (jambs) so trims can be drawn as continuous runs around floor edges and wall reveals.
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;
}
}