Editor utility for generating wall trim geometry. It provides methods to cap wall ends, create top caps and foot plates, baseboards, band courses, mouldings, pilasters and skirting runs, and builds mesh primitives (quads, prisms, boxes, polygons) from wall join and kit data.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchWallGen
{
static void CapEnds( ArchMesh canvas, float height, float half, ArchWallJoin join, ArchBrush reveal, ArchBrush exterior )
{
if ( join.StartJoined )
{
canvas.Quad(
new Vector3( join.StartMitre, half, 0f ),
new Vector3( join.StartExtend, -half, 0f ),
new Vector3( join.StartExtend, -half, height ),
new Vector3( join.StartMitre, half, height ),
exterior );
}
else if ( join.StartCapped )
{
canvas.Quad(
new Vector3( join.From, half, 0f ),
new Vector3( join.From, -half, 0f ),
new Vector3( join.From, -half, height ),
new Vector3( join.From, half, height ),
reveal );
}
if ( join.EndJoined )
{
canvas.Quad(
new Vector3( join.EndExtend, -half, 0f ),
new Vector3( join.EndMitre, half, 0f ),
new Vector3( join.EndMitre, half, height ),
new Vector3( join.EndExtend, -half, height ),
exterior );
}
else if ( join.EndCapped )
{
canvas.Quad(
new Vector3( join.To, -half, 0f ),
new Vector3( join.To, half, 0f ),
new Vector3( join.To, half, height ),
new Vector3( join.To, -half, height ),
reveal );
}
}
static void CapTop( ArchMesh canvas, ArchWall wall, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchWallJoin join, bool covered, float height, float half, ArchStyle style, ArchPalette[] chain )
{
if ( covered )
{
return;
}
var top = style.Brush( ArchSurface.WallCap, chain );
var coping = ArchWallSection.CapHeight( wall, kit );
if ( !wall.Cap || coping <= 0.1f )
{
Plate( canvas, join, half, height, top, true );
return;
}
var overhang = ArchWallSection.CapOverhang( wall, kit );
var spread = half > 0.01f ? (half + overhang) / half : 1f;
var capHeight = height + coping;
var foot = new List<Vector3>
{
new( join.StartExtend * spread, -half - overhang, height ),
new( join.Length + join.EndExtendCut * spread, -half - overhang, height ),
new( join.Length + join.EndMitreCut * spread, half + overhang, height ),
new( join.StartMitre * spread, half + overhang, height )
};
var outerFrom = foot[0];
var outerTo = foot[1];
var innerTo = foot[2];
var innerFrom = foot[3];
var span = outerTo.x - outerFrom.x;
foreach ( var run in FittingRuns( plan, kit, room, building, wall, outerFrom.x, outerTo.x, outerFrom.y, height, capHeight ) )
{
var start = span < 0.001f ? 0f : (run.From - outerFrom.x) / span;
var finish = span < 0.001f ? 1f : (run.To - outerFrom.x) / span;
var section = new List<Vector3>
{
Vector3.Lerp( outerFrom, outerTo, start ),
Vector3.Lerp( outerFrom, outerTo, finish ),
Vector3.Lerp( innerFrom, innerTo, finish ),
Vector3.Lerp( innerFrom, innerTo, start )
};
canvas.Prism( section, section.Select( point => point.WithZ( capHeight ) ).ToList(), top );
}
}
// A wall over open air has no slab under it either, so the same plate closes its foot.
static void CapFoot( ArchMesh canvas, ArchWallJoin join, float half, ArchStyle style, ArchPalette[] chain )
{
Plate( canvas, join, half, 0f, style.Brush( ArchSurface.WallCap, chain ), false );
}
static void Plate( ArchMesh canvas, ArchWallJoin join, float half, float z, ArchBrush brush, bool up )
{
var outline = new List<Vector3>
{
new( join.StartExtend, -half, z ),
new( join.EndExtend, -half, z ),
new( join.EndMitre, half, z ),
new( join.StartMitre, half, z )
};
if ( !up )
{
outline.Reverse();
}
canvas.Polygon( outline, brush );
}
static void Baseboards( ArchMesh canvas, ArchWall wall, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchWallJoin join, float half, ArchKit kit, IReadOnlyList<ArchOpening> openings, ArchBrush brush, IReadOnlyList<ArchWallRake> rakes, float finish )
{
var height = kit.BaseboardHeight;
var depth = kit.BaseboardDepth;
if ( height <= 0.1f || depth <= 0.05f )
{
return;
}
var blocked = openings
.Where( opening => opening.SillHeight < height )
.Select( opening => Gap( opening, kit ) )
.OrderBy( gap => gap.Left )
.ToList();
var thickness = ArchWallSection.Thickness( wall, kit );
var head = join.Head( depth );
var cursor = join.Foot( depth );
foreach ( var gap in blocked )
{
if ( gap.Left - cursor.Inner > 0.5f )
{
Skirt( canvas, cursor, Jamb( gap.Left, depth, thickness, kit, join, false, gap.Returns ), wall, room, building, plan, half, depth, height, kit, brush, rakes, finish );
}
cursor = Jamb( Math.Max( cursor.Inner, gap.Right ), depth, thickness, kit, join, true, gap.Returns );
}
if ( head.Inner - cursor.Inner > 0.5f )
{
Skirt( canvas, cursor, head, wall, room, building, plan, half, depth, height, kit, brush, rakes, finish );
}
}
static void BandCourses( ArchMesh canvas, ArchWall wall, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchWallJoin join, float height, float half,
IReadOnlyList<ArchWallModShape> proud, ArchBrush brush )
{
foreach ( var band in ArchWallTreatments.Ordered( wall.Bands, height ) )
{
var depth = MathF.Max( 0f, band.CourseDepth > 0.01f ? band.CourseDepth : kit.WallBandDepth );
var course = MathF.Max( 0f, band.CourseHeight > 0.01f ? band.CourseHeight : kit.WallBandHeight );
var at = band.At( height );
// A band at the head is the cap and stays; past it, another wall's.
if ( !band.Course || at > height + 0.05f || depth < 0.05f || course < 0.05f )
{
continue;
}
var bottom = band.Seat == BandSeat.Above ? at : at - course * 0.5f;
var foot = join.ExteriorFoot( depth );
var head = join.ExteriorHead( depth );
foreach ( var stretch in Returns( foot.Inner, head.Inner, depth, proud, bottom, bottom + course ) )
{
foreach ( var run in FittingRuns( plan, kit, room, building, wall, stretch.From, stretch.To, -half - stretch.Depth * 0.5f, bottom, bottom + course ) )
{
var start = MathF.Abs( run.From - foot.Inner ) < 0.01f
? join.ExteriorFoot( stretch.Depth )
: new ArchBandEnd( run.From, run.From, ArchBandStop.Cut );
var finish = MathF.Abs( run.To - head.Inner ) < 0.01f
? join.ExteriorHead( stretch.Depth )
: new ArchBandEnd( run.To, run.To, ArchBandStop.Cut );
ArchBandGen.Run( canvas, start, finish, ArchBandFrame.Local, -half, -stretch.Depth, bottom, bottom + course, brush );
}
}
}
}
static void Mouldings( ArchMesh canvas, ArchWall wall, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchWallJoin join, float height, float half,
IReadOnlyList<ArchWallModShape> outside, IReadOnlyList<ArchWallModShape> inside, ArchStyle style, ArchPalette[] chain )
{
foreach ( var trim in ArchWallTreatments.Standing( wall.Trims, height ) )
{
var bottom = trim.At( height );
var top = bottom + trim.Rise;
var bevel = MathF.Min( trim.Bevel, trim.Rise * 0.45f );
var spread = half > 0.01f ? (half + trim.Depth) / half : 1f;
var start = trim.Interior
? join.Foot( trim.Depth ).Outer
: join.StartExtend * spread;
var finish = trim.Interior
? join.Head( trim.Depth ).Outer
: join.Length + join.EndExtendCut * spread;
if ( finish - start < 0.5f )
{
continue;
}
var face = trim.Interior ? half : -half;
var brush = style.Brush( trim.Field, chain );
// A moulding meets whatever stands on its own face - the room's mouldings return around a
// pier standing in the room, not around one on the street elevation behind it.
var returns = Returns( start, finish, trim.Depth, trim.Interior ? inside : outside, bottom, top );
foreach ( var stretch in returns )
{
var reach = trim.Interior ? half + stretch.Depth : -half - stretch.Depth;
foreach ( var run in FittingRuns( plan, kit, room, building, wall, stretch.From, stretch.To, (face + reach) * 0.5f, bottom, top ) )
{
canvas.Prism(
Section( run.From, face, reach, bottom, top, bevel ),
Section( run.To, face, reach, bottom, top, bevel ),
brush );
}
}
}
}
static List<Vector3> Section( float along, float face, float proud, float bottom, float top, float bevel )
{
return new List<Vector3>
{
new( along, face, bottom ),
new( along, proud, bottom + bevel ),
new( along, proud, top - bevel ),
new( along, face, top )
};
}
static void Pilasters( ArchMesh canvas, ArchWall wall, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchWallJoin join, float height, float half, ArchBrush brush )
{
var run = wall.Pilasters;
if ( run is null || !run.Stands )
{
return;
}
var top = height - MathF.Max( 0f, run.Headroom );
var foot = MathF.Max( 0f, run.Foot );
var spread = half > 0.01f ? (half + run.Depth) / half : 1f;
var start = join.StartExtend * spread;
var finish = join.Length + join.EndExtendCut * spread;
var span = finish - start;
if ( top - foot < 1f || span < 1f )
{
return;
}
var strip = MathF.Min( run.Width, span );
// Explicit coordinates, not Beam: Beam offsets along ArchRegion.Outward and would stand the
// whole column line inside the room.
foreach ( var centre in ArchWallSection.PilasterCentres( run, start, finish ) )
{
foreach ( var stretch in FittingRuns( plan, kit, room, building, wall, centre - strip * 0.5f, centre + strip * 0.5f, -half - run.Depth * 0.5f, foot, top ) )
{
canvas.Box(
new Vector3( stretch.From, -half - run.Depth, foot ),
new Vector3( stretch.To, -half, top ),
brush );
}
}
}
static ArchBandEnd Square( float at ) => new( at, at, ArchBandStop.Cut );
// A board into an architrave dies square. Where another wall lands exactly on the opening's edge
// the hole is a mouth, not a window: that wall's own board turns the jamb, and the two meet on
// the mitre with the face running past - the same splay ArchWallJoin gives a wrapping wall end.
static ArchBandEnd Jamb( float at, float depth, float thickness, ArchKit kit, ArchWallJoin join, bool starting, bool returns )
{
if ( !join.WrapsAt( at ) )
{
return returns
? new ArchBandEnd(
at,
starting ? at - depth : at + depth,
ArchBandStop.Returned,
MathF.Max( 0f, thickness - ArchContact.Bite( kit ) ) )
: Square( at );
}
return new ArchBandEnd( at, starting ? at - depth : at + depth, ArchBandStop.Mitred );
}
static BaseboardGap Gap( ArchOpening opening, ArchKit kit )
{
var cased = Cased( opening, kit );
return new BaseboardGap( cased.Left, cased.Right, opening.Kind == OpeningKind.Archway && !opening.Cased );
}
// Hole plus architrave: a board off the hole alone dies behind it.
static (float Left, float Right) Cased( ArchOpening opening, ArchKit kit )
{
var casing = opening.Cased ? MathF.Max( 0f, kit.CasingWidth ) : 0f;
return (opening.Left - casing, opening.Right + casing);
}
readonly record struct BaseboardGap( float Left, float Right, bool Returns );
// The level board runs the whole stretch; a rake laps over it.
static void Skirt( ArchMesh canvas, ArchBandEnd from, ArchBandEnd to, ArchWall wall, ArchRoom room, ArchBuilding building, ArchPlan plan, float half, float depth, float height, ArchKit kit, ArchBrush brush, IReadOnlyList<ArchWallRake> rakes, float finish )
{
Run( canvas, from, to, wall, room, building, plan, half, depth, height, kit, brush, finish );
foreach ( var rake in Within( rakes, from.Inner, to.Inner ) )
{
Rake( canvas, rake, half, depth, height, brush );
}
}
// Past the wall head the board carries on with its top edge cut flat.
static void Rake( ArchMesh canvas, ArchWallRake rake, float half, float depth, float height, ArchBrush brush )
{
if ( rake.To - rake.From < 0.5f )
{
return;
}
var head = rake.Rise + height;
var topFrom = rake.LiftFrom + head;
var topTo = rake.LiftTo + head;
var breaks = ArchClearance.Cross( topFrom, topTo, rake.Ceiling );
if ( breaks.Clears )
{
Sloped( canvas, rake.From, rake.To, half, depth, rake.LiftFrom, rake.LiftTo, topFrom, topTo, brush );
return;
}
if ( breaks.Blocked )
{
Sloped( canvas, rake.From, rake.To, half, depth, rake.LiftFrom, rake.LiftTo, rake.Ceiling, rake.Ceiling, brush );
return;
}
var split = rake.From + (rake.To - rake.From) * breaks.At;
var lift = rake.LiftFrom + (rake.LiftTo - rake.LiftFrom) * breaks.At;
Sloped( canvas, rake.From, split, half, depth, rake.LiftFrom, lift, topFrom, rake.Ceiling, brush );
Sloped( canvas, split, rake.To, half, depth, lift, rake.LiftTo, rake.Ceiling, rake.Ceiling, brush );
}
static void Sloped( ArchMesh canvas, float from, float to, float half, float depth, float lowFrom, float lowTo, float highFrom, float highTo, ArchBrush brush )
{
ArchBandGen.Raked( canvas, Square( from ), Square( to ), ArchBandFrame.Local, half, depth, lowFrom, lowTo, highFrom, highTo, brush );
}
public static IReadOnlyList<ArchWallRake> Skirting( ArchWall wall, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, float half, float wallHeight, ArchAnswers answers = null )
{
return (answers ?? ArchAnswers.Load()).WallRakes( wall, room, building, plan, kit, half, wallHeight );
}
static IEnumerable<ArchWallRake> Within( IReadOnlyList<ArchWallRake> rakes, float from, float to )
{
foreach ( var rake in rakes ?? Array.Empty<ArchWallRake>() )
{
var start = MathF.Max( rake.From, from );
var end = MathF.Min( rake.To, to );
if ( end - start >= 0.5f )
{
yield return new ArchWallRake( start, end, Lift( rake, start ), Lift( rake, end ), rake.Rise, rake.Ceiling );
}
}
}
static float Lift( ArchWallRake rake, float along )
{
var span = rake.To - rake.From;
return span < 0.01f
? rake.LiftFrom
: rake.LiftFrom + (along - rake.From) / span * (rake.LiftTo - rake.LiftFrom);
}
}