Editor-side static helpers for generating wall geometry. It builds cladding (weatherboard or profiled ribs), courses, ribs, end caps, wainscoting and dado panels by emitting quads, polygons and boxes into an ArchMesh using parameters from an ArchKit and ArchBrushes.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchWallGen
{
// Lap the field, don't replace it: panel carries material, profile the shadow.
static void Clad( ArchMesh canvas, Panel panel, float half, ArchKit kit, WallCladding cladding, ArchBrush brush )
{
switch ( cladding )
{
case WallCladding.Weatherboard:
Courses( canvas, panel, half, kit, brush );
break;
case WallCladding.Profiled:
Ribs( canvas, panel, half, kit, brush );
break;
}
}
// Ribs index off x = 0, or the sheeting steps at every opening.
static void Ribs( ArchMesh canvas, Panel panel, float half, ArchKit kit, ArchBrush brush )
{
var pitch = MathF.Max( 2f, kit.SheetPitch );
var crest = Math.Clamp( kit.SheetCrest, 0.5f, pitch * 0.6f );
var proud = MathF.Max( 0.1f, kit.SheetProud );
var web = (pitch - crest) * 0.3f;
var valley = pitch - crest - web * 2f;
if ( panel.MaxZ - panel.MinZ < 0.05f || panel.MaxX - panel.MinX < 0.05f )
{
return;
}
var first = MathF.Floor( panel.MinX / pitch );
var last = MathF.Ceiling( panel.MaxX / pitch );
for ( var index = first; index < last; index++ )
{
var at = index * pitch;
var flat = -half;
var high = -half - proud;
Rib( canvas, panel, at, flat, at + valley, flat, brush );
Rib( canvas, panel, at + valley, flat, at + valley + web, high, brush );
Rib( canvas, panel, at + valley + web, high, at + valley + web + crest, high, brush );
Rib( canvas, panel, at + valley + web + crest, high, at + pitch, flat, brush );
}
}
static void Rib( ArchMesh canvas, Panel panel, float fromX, float fromY, float toX, float toY, ArchBrush brush )
{
var minX = MathF.Max( panel.MinX, fromX );
var maxX = MathF.Min( panel.MaxX, toX );
if ( maxX - minX < 0.01f )
{
return;
}
var span = toX - fromX;
float Depth( float x ) => span < 0.001f ? fromY : fromY + (x - fromX) / span * (toY - fromY);
var near = Depth( minX );
var far = Depth( maxX );
canvas.Quad(
new Vector3( minX, near, panel.MinZ ),
new Vector3( maxX, far, panel.MinZ ),
new Vector3( maxX, far, panel.MaxZ ),
new Vector3( minX, near, panel.MaxZ ),
brush );
}
// Courses index off z = 0, or the coursing would step at every opening.
static void Courses( ArchMesh canvas, Panel panel, float half, ArchKit kit, ArchBrush brush )
{
var course = MathF.Max( 2f, kit.SidingCourse );
var proud = MathF.Max( 0.1f, kit.SidingProud );
if ( panel.MaxX - panel.MinX < 0.05f )
{
return;
}
var first = MathF.Floor( panel.MinZ / course );
var last = MathF.Ceiling( panel.MaxZ / course );
for ( var index = first; index < last; index++ )
{
var boardBottom = index * course;
var bottom = MathF.Max( panel.MinZ, boardBottom );
var top = MathF.Min( panel.MaxZ, boardBottom + course );
if ( top - bottom < 0.05f )
{
continue;
}
// The face is a ramp; a clipped course samples it, not the full lap.
var nearProud = proud * (1f - (bottom - boardBottom) / course);
var farProud = proud * (1f - (top - boardBottom) / course);
canvas.Quad(
new Vector3( panel.MinX, -half - nearProud, bottom ),
new Vector3( panel.MaxX, -half - nearProud, bottom ),
new Vector3( panel.MaxX, -half - farProud, top ),
new Vector3( panel.MinX, -half - farProud, top ),
brush );
canvas.Quad(
new Vector3( panel.MinX, -half, bottom ),
new Vector3( panel.MaxX, -half, bottom ),
new Vector3( panel.MaxX, -half - nearProud, bottom ),
new Vector3( panel.MinX, -half - nearProud, bottom ),
brush );
EndCap( canvas, panel.MinX, half, bottom, top, nearProud, farProud, false, brush );
EndCap( canvas, panel.MaxX, half, bottom, top, nearProud, farProud, true, brush );
}
}
// The flush top edge collapses the section; a degenerate quad breaks the normal.
static void EndCap( ArchMesh canvas, float x, float half, float bottom, float top, float nearProud, float farProud, bool flip, ArchBrush brush )
{
var points = new List<Vector3>
{
new( x, -half, bottom ),
new( x, -half - nearProud, bottom ),
new( x, -half - farProud, top ),
new( x, -half, top )
};
for ( var index = points.Count - 1; index > 0; index-- )
{
if ( (points[index] - points[index - 1]).Length < 0.01f )
{
points.RemoveAt( index );
}
}
if ( (points[^1] - points[0]).Length < 0.01f )
{
points.RemoveAt( points.Count - 1 );
}
canvas.Polygon( points, brush, flip );
}
static void Wainscot( ArchMesh canvas, float from, float to, float half, ArchKit kit, IReadOnlyList<ArchOpening> openings, ArchBrush panelling, ArchBrush rail, float finish )
{
var top = kit.WainscotHeight + finish;
var skirt = MathF.Max( 0f, kit.BaseboardHeight ) + finish;
var cap = MathF.Max( 0.5f, kit.ChairRailHeight );
var proud = MathF.Max( 0.2f, kit.WainscotProud );
var member = MathF.Max( 1f, kit.WainscotStile );
var field = top - cap;
if ( field - skirt < member * 2f + 2f )
{
return;
}
var blocked = openings
.Where( opening => opening.SillHeight < field )
.Select( opening => Cased( opening, kit ) )
.OrderBy( gap => gap.Left )
.ToList();
var cursor = from;
foreach ( var gap in blocked )
{
if ( gap.Left - cursor > 6f )
{
Dado( canvas, cursor, gap.Left, half, skirt, field, top, proud, member, kit.WainscotBay, kit.ChairRailDepth, panelling, rail );
}
cursor = MathF.Max( cursor, gap.Right );
}
if ( to - cursor > 6f )
{
Dado( canvas, cursor, to, half, skirt, field, top, proud, member, kit.WainscotBay, kit.ChairRailDepth, panelling, rail );
}
}
static void Dado(
ArchMesh canvas,
float from,
float to,
float half,
float skirt,
float field,
float top,
float proud,
float member,
float bay,
float capDepth,
ArchBrush panelling,
ArchBrush rail )
{
var recess = proud * 0.4f;
canvas.Box( new Vector3( from, half, skirt ), new Vector3( to, half + recess, field ), panelling, BoxFaces.All & ~BoxFaces.Front );
canvas.Box( new Vector3( from, half, skirt ), new Vector3( to, half + proud, skirt + member ), panelling );
canvas.Box( new Vector3( from, half, field - member ), new Vector3( to, half + proud, field ), panelling );
foreach ( var offset in ArchDivide.AtMost( to - from, MathF.Max( 8f, bay ) ).Nodes )
{
var left = Math.Clamp( from + offset - member * 0.5f, from, to - member );
canvas.Box( new Vector3( left, half, skirt ), new Vector3( left + member, half + proud, field ), panelling );
}
canvas.Box( new Vector3( from, half, field ), new Vector3( to, half + capDepth, top ), rail );
}
}