Editor/Wall/ArchWallOpenings.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Coordinates are the wall's own: x along, y across, z up.
public readonly struct ArchOpeningGrille {
public OpeningFurniture Furniture { get; init; }
public ArchSurface Surface { get; init; }
public bool Upright { get; init; }
public float Border { get; init; }
public float Member { get; init; }
public float Gap { get; init; }
public float Face { get; init; }
public float Depth { get; init; }
public Vector2 Min { get; init; }
public Vector2 Max { get; init; }
public Vector2 InnerMin => new( Min.x + Border, Min.y + Border );
public Vector2 InnerMax => new( Max.x - Border, Max.y - Border );
public float Pitch => Member + Gap;
public bool Stands => Furniture != OpeningFurniture.None
&& InnerMax.x - InnerMin.x > Member
&& InnerMax.y - InnerMin.y > Member;
public int Members => ArchDivide.AtMost( Upright ? InnerMax.x - InnerMin.x : InnerMax.y - InnerMin.y, Pitch ).Count;
}
public static class ArchWallOpenings {
public static ArchSurface BooleanRevealSurface => ArchSurface.Trim;
public static List<List<Vector2>> BooleanRevealLoops( IEnumerable<ArchOpening> openings, float wallHeight ) {
return ArchFootprint.Union( openings
.Select( opening => {
var bottom = Math.Clamp( opening.SillHeight, 0f, wallHeight );
var top = Math.Clamp( opening.Top, 0f, wallHeight );
return (IReadOnlyList<Vector2>)ArchFootprint.Rect(
new Vector2( opening.Left, bottom ),
new Vector2( opening.Right, top ) );
} )
.Where( loop => loop[2].x - loop[0].x > 0.01f && loop[2].y - loop[0].y > 0.01f ) ).ToList();
}
public static void BooleanReveals( ArchMesh canvas, IEnumerable<ArchOpening> openings, float half, float wallHeight, ArchBrush brush ) {
foreach ( var loop in BooleanRevealLoops( openings, wallHeight ) ) {
for ( var index = 0; index < loop.Count; index++ ) {
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
var horizontal = MathF.Abs( from.y - to.y ) < 0.01f;
if ( horizontal && (MathF.Abs( from.y ) < 0.01f || MathF.Abs( from.y - wallHeight ) < 0.01f) ) {
continue;
}
canvas.Quad(
new Vector3( from.x, half, from.y ),
new Vector3( from.x, -half, from.y ),
new Vector3( to.x, -half, to.y ),
new Vector3( to.x, half, to.y ),
brush );
}
}
}
public static List<(Vector2 Min, Vector2 Max)> BooleanCasingBoards( IEnumerable<ArchOpening> openings, ArchKit kit, float wallHeight ) {
var boards = new List<(Vector2 Min, Vector2 Max)>();
var cased = openings.Where( opening => opening.Cased ).ToList();
var authored = cased.Select( opening => opening.CasingWidth ).FirstOrDefault( value => value > 0.1f );
var width = authored > 0.1f ? authored : kit.CasingWidth;
if ( cased.Count == 0 || width <= 0.1f || kit.CasingDepth <= 0.05f ) {
return boards;
}
var loops = BooleanRevealLoops( cased, wallHeight );
var region = loops.Select( loop => (IReadOnlyList<Vector2>)loop ).ToList();
var bite = ArchLap.Bite( kit );
foreach ( var loop in loops ) {
for ( var index = 0; index < loop.Count; index++ ) {
if ( Architrave( region, loop[index], loop[(index + 1) % loop.Count], wallHeight, width, bite ) is { } board ) {
boards.Add( board );
}
}
}
return boards;
}
public static void BooleanCasing( ArchMesh canvas, IReadOnlyList<ArchOpening> openings, ArchKit kit, float half, float wallHeight, ArchBrush brush ) {
var boards = BooleanCasingBoards( openings, kit, wallHeight );
foreach ( var face in new[] { half, -half } ) {
var sign = Math.Sign( face );
var back = ArchLap.Bury( kit, face, sign );
var near = MathF.Min( back, face + sign * kit.CasingDepth );
var far = MathF.Max( back, face + sign * kit.CasingDepth );
foreach ( var board in boards ) {
canvas.Box(
new Vector3( board.Min.x, near, board.Min.y ),
new Vector3( board.Max.x, far, board.Max.y ),
brush );
}
}
}
static (Vector2 Min, Vector2 Max)? Architrave(
IReadOnlyList<IReadOnlyList<Vector2>> region,
Vector2 from,
Vector2 to,
float wallHeight,
float width,
float bite ) {
var span = to - from;
var middle = (from + to) * 0.5f;
if ( span.Length < 0.01f ) {
return null;
}
var outward = new Vector2( span.y, -span.x ).Normal;
if ( ArchFootprint.Encloses( region, middle + outward * 0.1f ) ) {
outward = -outward;
}
var low = new Vector2( MathF.Min( from.x, to.x ), MathF.Min( from.y, to.y ) );
var high = new Vector2( MathF.Max( from.x, to.x ), MathF.Max( from.y, to.y ) );
if ( MathF.Abs( span.x ) < 0.01f ) {
var jamb = high.x + outward.x * width;
// Jambs at the floor line extend below it — the well trim stands proud
var foot = low.y < 0.01f ? -width : low.y - width;
return (new Vector2( MathF.Min( jamb, high.x - outward.x * bite ), foot ),
new Vector2( MathF.Max( jamb, high.x - outward.x * bite ), MathF.Min( wallHeight, high.y + width ) ));
}
if ( MathF.Abs( high.y ) < 0.01f || MathF.Abs( high.y - wallHeight ) < 0.01f ) {
return null;
}
var rail = high.y + outward.y * width;
return (new Vector2( low.x - width, MathF.Min( rail, high.y - outward.y * bite ) ),
new Vector2( high.x + width, MathF.Max( rail, high.y - outward.y * bite ) ));
}
public static void Reveals( ArchMesh canvas, ArchOpening opening, float half, float wallHeight, bool sill, ArchBrush brush ) {
var left = opening.Left;
var right = opening.Right;
var bottom = Math.Max( 0f, opening.SillHeight );
var top = Math.Min( wallHeight, opening.Top );
if ( top <= bottom ) {
return;
}
canvas.Quad(
new Vector3( left, -half, bottom ),
new Vector3( left, half, bottom ),
new Vector3( left, half, top ),
new Vector3( left, -half, top ),
brush );
canvas.Quad(
new Vector3( right, half, bottom ),
new Vector3( right, -half, bottom ),
new Vector3( right, -half, top ),
new Vector3( right, half, top ),
brush );
canvas.Quad(
new Vector3( left, half, top ),
new Vector3( right, half, top ),
new Vector3( right, -half, top ),
new Vector3( left, -half, top ),
brush );
if ( bottom > 0.01f && !sill ) {
canvas.Quad(
new Vector3( left, -half, bottom ),
new Vector3( right, -half, bottom ),
new Vector3( right, half, bottom ),
new Vector3( left, half, bottom ),
brush );
}
}
public static void Casing( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchBrush brush ) {
var width = opening.CasingWidth > 0f ? opening.CasingWidth : kit.CasingWidth;
var depth = kit.CasingDepth;
if ( width <= 0.1f || depth <= 0.05f ) {
return;
}
var left = opening.Left;
var right = opening.Right;
var bottom = Math.Max( 0f, opening.SillHeight );
var top = Math.Min( wallHeight, opening.Top );
var foot = Math.Max( 0f, bottom - width );
var headWidth = CasingHead( opening, kit );
var headDepth = opening.IsGlazed ? depth * 1.7f : depth;
var headRun = opening.IsGlazed ? width * 1.6f : width;
var bite = ArchLap.Bite( kit );
foreach ( var face in new[] { half, -half } ) {
var sign = Math.Sign( face );
var back = ArchLap.Bury( kit, face, sign );
var near = Math.Min( back, face + sign * depth );
var far = Math.Max( back, face + sign * depth );
var headNear = Math.Min( back, face + sign * headDepth );
var headFar = Math.Max( back, face + sign * headDepth );
canvas.Box( new Vector3( left - width, near, foot ), new Vector3( left + bite, far, top ), brush );
canvas.Box( new Vector3( right - bite, near, foot ), new Vector3( right + width, far, top ), brush );
canvas.Box( new Vector3( left - headRun, headNear, top - bite ), new Vector3( right + headRun, headFar, top + headWidth ), brush );
if ( bottom > 0.01f ) {
canvas.Box( new Vector3( left, near, foot ), new Vector3( right, far, bottom + bite ), brush );
}
}
}
public static float CasingHead( ArchOpening opening, ArchKit kit ) {
var width = opening.CasingWidth > 0f ? opening.CasingWidth : kit.CasingWidth;
if ( !opening.Cased || width <= 0.1f || kit.CasingDepth <= 0.05f ) {
return 0f;
}
return opening.IsGlazed ? width * 1.7f : width;
}
public static float Springing( ArchOpening opening, ArchKit kit, float wallHeight ) {
return MathF.Min( wallHeight, opening.Top ) + CasingHead( opening, kit );
}
public static void Hood( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchBrush brush ) {
var hood = opening.Hooded;
if ( hood == OpeningHood.None ) {
return;
}
var seat = Springing( opening, kit, wallHeight );
var rise = MathF.Max( 1f, kit.HoodHeight );
var overhang = MathF.Max( 0f, kit.HoodOverhang );
var projection = MathF.Max( 1f, kit.HoodProjection );
var bite = ArchLap.Bite( kit );
var back = ArchLap.Bury( kit, -half, -1 );
if ( seat + rise > wallHeight ) {
return;
}
Corbels( canvas, opening, kit, half, seat, projection, brush );
canvas.Box(
new Vector3( opening.Left - overhang * 0.55f, -half - projection * 0.55f, seat - bite ),
new Vector3( opening.Right + overhang * 0.55f, back, seat + rise * 0.45f ),
brush );
canvas.Box(
new Vector3( opening.Left - overhang, -half - projection, seat + rise * 0.45f ),
new Vector3( opening.Right + overhang, back, seat + rise ),
brush );
if ( hood == OpeningHood.Pediment ) {
Pediment( canvas, opening, kit, half, wallHeight, seat + rise, back, brush );
}
}
static void Corbels( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float seat, float projection, ArchBrush brush ) {
var width = MathF.Max( 0f, kit.HoodCorbel );
var drop = MathF.Max( 0f, kit.HoodCorbelDrop );
var back = ArchLap.Bury( kit, -half, -1 );
var foot = seat - drop;
if ( width < 0.1f || drop < 0.1f || foot <= MathF.Max( 0f, opening.SillHeight ) ) {
return;
}
foreach ( var station in new[] { opening.Left, opening.Right - width } ) {
canvas.Box(
new Vector3( station, -half - projection * 0.55f, foot ),
new Vector3( station + width, back, seat ),
brush );
}
}
static void Pediment( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, float seat, float back, ArchBrush brush ) {
var rise = MathF.Max( 1f, kit.PedimentRise );
var overhang = MathF.Max( 0f, kit.HoodOverhang );
var left = opening.Left - overhang;
var right = opening.Right + overhang;
var apex = (left + right) * 0.5f;
var near = -half - MathF.Max( 1f, kit.HoodProjection ) * 0.7f;
if ( seat + rise > wallHeight || right - left < 2f ) {
return;
}
canvas.Rake( new Vector2( left, 0f ), new Vector2( apex, 0f ), near, back, seat, seat, seat, seat + rise, brush );
canvas.Rake( new Vector2( apex, 0f ), new Vector2( right, 0f ), near, back, seat, seat, seat + rise, seat, brush );
}
public static void Sill( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, ArchBrush brush ) {
var depth = kit.SillDepth;
var thickness = kit.SillThickness;
var overshoot = kit.CasingWidth;
var outward = opening.IsGlazed ? depth * 2.2f : depth;
canvas.Box(
new Vector3( opening.Left - overshoot, -half - outward, opening.SillHeight - thickness ),
new Vector3( opening.Right + overshoot, half + depth, opening.SillHeight ),
brush );
if ( !opening.IsGlazed ) {
return;
}
canvas.Box(
new Vector3( opening.Left - overshoot, -half - outward, opening.SillHeight - thickness - 1f ),
new Vector3( opening.Right + overshoot, -half - outward + 1.3f, opening.SillHeight - thickness ),
brush );
}
public static ArchOpeningGrille Grille( ArchOpening opening, ArchKit kit, float half, float wallHeight ) {
var fitted = opening.Fitted == OpeningFurniture.Leaves ? OpeningFurniture.None : opening.Fitted;
var boarded = fitted == OpeningFurniture.Boarded;
var bite = ArchLap.Bite( kit );
var proud = MathF.Max( bite, kit.GrilleProud );
var bottom = MathF.Max( 0f, opening.SillHeight );
var top = MathF.Min( wallHeight, opening.Top );
var span = ArchLap.Lapped( kit, opening.Left, opening.Right );
var rise = ArchLap.Lapped( kit, bottom, top );
return new ArchOpeningGrille {
Furniture = fitted,
Surface = fitted == OpeningFurniture.Shutter ? ArchSurface.Shutter : ArchSurface.Grille,
Upright = fitted is OpeningFurniture.Bars or OpeningFurniture.Gate,
Border = boarded ? 0f : MathF.Max( 0f, kit.GrilleFrame ),
Member = MathF.Max( 1f, boarded ? kit.FloorBoardWidth : kit.GrilleBar ),
Gap = MathF.Max( 0f, boarded ? kit.FloorBoardGap : kit.GrilleGap ),
Face = fitted == OpeningFurniture.Gate ? -half + bite : -half - proud,
Depth = proud + bite,
Min = boarded ? new Vector2( span.From, rise.From ) : new Vector2( opening.Left + bite, bottom + bite ),
Max = boarded ? new Vector2( span.To, rise.To ) : new Vector2( opening.Right - bite, top - bite )
};
}
public static IEnumerable<ArchOpeningGrille> Leaves( ArchOpening opening, ArchKit kit, float half, float wallHeight ) {
if ( opening.Fitted != OpeningFurniture.Leaves ) {
yield break;
}
var bite = ArchLap.Bite( kit );
var proud = MathF.Max( bite, kit.LeafProud );
var bottom = MathF.Max( 0f, opening.SillHeight );
var top = MathF.Min( wallHeight, opening.Top );
var leaf = opening.Width * 0.5f;
foreach ( var outer in new[] { opening.Left - leaf, opening.Right } ) {
yield return new ArchOpeningGrille {
Furniture = OpeningFurniture.Leaves,
Surface = ArchSurface.Shutter,
Upright = false,
Border = MathF.Max( 0f, kit.GrilleFrame ),
Member = MathF.Max( 0.4f, kit.LeafSlat ),
Gap = MathF.Max( 0f, kit.LeafGap ),
Face = -half - proud,
Depth = MathF.Max( 0.4f, kit.LeafThickness ),
Min = new Vector2( outer, bottom ),
Max = new Vector2( outer + leaf, top )
};
}
}
public static void Furniture( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchStyle style, ArchPalette[] chain ) {
var standing = Leaves( opening, kit, half, wallHeight )
.Append( Grille( opening, kit, half, wallHeight ) )
.Where( fitting => fitting.Stands )
.ToList();
if ( standing.Count == 0 ) {
return;
}
using ( canvas.Part( ArchPieces.Bars ) ) {
foreach ( var fitting in standing ) {
var brush = style.Brush( fitting.Surface, chain );
GrilleFrame( canvas, fitting, kit, brush );
GrilleMembers( canvas, fitting, brush );
}
}
}
static void GrilleFrame( ArchMesh canvas, ArchOpeningGrille grille, ArchKit kit, ArchBrush brush ) {
if ( grille.Border < 0.05f ) {
return;
}
var bite = ArchLap.Bite( kit );
var inner = grille.InnerMin;
var outer = grille.InnerMax;
var near = grille.Face;
var far = grille.Face + grille.Depth;
canvas.Box( new Vector3( grille.Min.x, near, grille.Min.y ), new Vector3( inner.x, far, grille.Max.y ), brush );
canvas.Box( new Vector3( outer.x, near, grille.Min.y ), new Vector3( grille.Max.x, far, grille.Max.y ), brush );
canvas.Box( new Vector3( inner.x - bite, near, grille.Min.y ), new Vector3( outer.x + bite, far, inner.y ), brush );
canvas.Box( new Vector3( inner.x - bite, near, outer.y ), new Vector3( outer.x + bite, far, grille.Max.y ), brush );
}
static void GrilleMembers( ArchMesh canvas, ArchOpeningGrille grille, ArchBrush brush ) {
var inner = grille.InnerMin;
var outer = grille.InnerMax;
var region = new[] { (IReadOnlyList<Vector2>)ArchFootprint.Rect( inner, outer ) };
var spec = new ArchPlankSpec {
Width = grille.Member,
Gap = grille.Gap,
Thickness = grille.Depth,
// Four times the diagonal, so no stagger joint can land inside the hole: a bar is one length of steel
// and a shutter slat runs the whole width of the curtain.
Length = (outer - inner).Length * 4f,
Top = grille.Face + grille.Depth,
Yaw = grille.Upright ? 90f : 0f,
Soffit = true
};
ArchPlanks.Fill( canvas, region, null, spec, brush, ArchPlankPlane.Upright );
}
public static float Threshold( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float floor, ArchBrush brush ) {
var proud = MathF.Max( 0f, kit.ThresholdProud );
if ( !Saddled( opening ) || proud < 0.05f ) {
return 0f;
}
var lap = MathF.Max( 0f, kit.ThresholdLap );
var top = floor + proud;
var span = ArchLap.Lapped( kit, opening.Left, opening.Right );
canvas.Box(
new Vector3( span.From, -half - lap, ArchLap.Bury( kit, 0f, 1f ) ),
new Vector3( span.To, half + lap, top ),
brush );
return top;
}
static bool Saddled( ArchOpening opening ) {
return opening.Cased && opening.SillHeight < 0.5f && opening.Kind != OpeningKind.Garage;
}
public static void Leaves( List<ArchDoorRequest> doors, ArchOpening opening, ArchWall wall, ArchKit kit, float saddle, ArchBrush brush ) {
if ( doors is null ) {
return;
}
var clearance = saddle + MathF.Max( 0f, kit.DoorUndercut );
var foot = opening.SillHeight + clearance;
var height = opening.Height - clearance;
var thickness = kit.DoorLeafThickness;
if ( height < 1f ) {
return;
}
if ( opening.Kind == OpeningKind.DoubleDoor ) {
var leaf = opening.Width * 0.5f;
doors.Add( Request( opening, wall, new Vector3( opening.Left, 0f, foot ), leaf, height, thickness, false, brush ) );
doors.Add( Request( opening, wall, new Vector3( opening.Right, 0f, foot ), leaf, height, thickness, true, brush ) );
return;
}
var hingeX = opening.FlipHinge ? opening.Right : opening.Left;
doors.Add( Request( opening, wall, new Vector3( hingeX, 0f, foot ), opening.Width, height, thickness, opening.FlipHinge, brush ) );
}
static ArchDoorRequest Request( ArchOpening opening, ArchWall wall, Vector3 hinge, float width, float height, float thickness, bool mirrored, ArchBrush brush ) {
return new ArchDoorRequest {
Opening = opening,
Wall = wall,
HingeLocal = hinge,
LeafWidth = width,
LeafHeight = height,
LeafThickness = thickness,
Mirrored = mirrored,
Brush = brush,
Art = opening.LeafArt,
MaterialGroup = opening.LeafMaterialGroup
};
}
}