Editor/Wall/ArchWallTrims.cs
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 );
// A crowned head is finished by the cornice's own top course; laying a coping too is two solids sharing
// a plane. The plate stays either way - it closes the wall's own box.
if ( !wall.Cap || coping <= 0.1f || Crowned( wall, kit ) ) {
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 );
var blocked = proud
.Where( shape => shape.Part.Field == ArchSurface.WallBase && shape.Spans( bottom, bottom + course, height ) )
.ToList();
foreach ( var stretch in Returns( foot.Inner, head.Inner, depth, proud, bottom, bottom + course, height ) ) {
var centre = (stretch.From + stretch.To) * 0.5f;
if ( blocked.Any( shape => centre > shape.From && centre < shape.To ) ) {
continue;
}
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 );
if ( band.Dentils ) {
Dentils( canvas, kit, run.From, run.To, half, stretch.Depth, bottom, brush );
}
}
}
}
}
// The blocks a dentilled course stands on, counted off the run by ArchDivide rather than typed against the
// band. They do not mitre and they do not return: a dentil is a block, and a block that turned a corner
// would be a moulding pretending to be one.
static void Dentils( ArchMesh canvas, ArchKit kit, float from, float to, float half, float depth, float seat, ArchBrush brush ) {
var width = MathF.Max( 0.4f, kit.DentilWidth );
var gap = MathF.Max( 0.2f, kit.DentilGap );
var drop = MathF.Max( 0.4f, kit.DentilDrop );
var span = to - from;
if ( span < width || seat - drop < 0f ) {
return;
}
var division = ArchDivide.AtMost( span, width + gap );
var back = ArchLap.Bury( kit, -half, -1 );
var face = -half - depth * 0.7f;
for ( var block = 0; block < division.Count; block++ ) {
// Centred in its own step, so a run that does not divide evenly loses the slack at both ends rather
// than standing a half block against the corner.
var at = from + division.Step * (block + 0.5f) - width * 0.5f;
canvas.Box( new Vector3( at, face, seat - drop ), new Vector3( at + width, back, seat ), 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 ) {
var lined = ArchWallSection.Lined( wall, room );
foreach ( var trim in ArchWallTreatments.Standing( wall.Trims, height ) ) {
if ( trim.Interior && !lined ) {
continue;
}
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 ? 1f : -1f) * (half - ArchLap.Bite( kit ));
var brush = style.Brush( trim.Field, chain );
var shapes = trim.Interior ? inside : outside;
var returning = !trim.Interior && trim.Field == ArchSurface.WallBase
? shapes.Where( shape => shape.Part.Field != ArchSurface.WallBase ).ToList()
: shapes;
// 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, returning, bottom, top, height );
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 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 - ArchLap.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);
}
}