Editor-side generator for wall geometry and fittings. It builds wall mesh panels, trims, openings, windows, doors, baseboards and related decorative bands using room/building/style/kit data and writes quads to an ArchMesh canvas.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchWallGen
{
public static void Build(
ArchMesh canvas,
ArchWall wall,
ArchRoom room,
ArchBuilding building,
ArchKit kit,
ArchStyle style,
List<ArchDoorRequest> doors,
List<ArchGlassRequest> glazing,
ArchConnectionService connections = null,
ArchPlan plan = null )
{
if ( connections is null )
{
connections = new ArchConnectionService( building, kit );
}
var floorAbove = ArchStoreyGeometry.FloorAbove( room, building, kit, wall.Start, wall.End );
var storey = floorAbove > room.BaseHeight + 0.5f ? floorAbove - room.BaseHeight : 0f;
var height = ArchWallSection.Height( wall, room, kit );
var thickness = ArchWallSection.Thickness( wall, kit );
var length = wall.Length;
// A wall authored short of the ceiling is a half wall: it keeps the head it was given and is
// finished by its own cap, where a full-height one is finished by whatever stands over it.
var freeHead = wall.Height > 0.5f && wall.Height < MathF.Max( storey, ArchWallSection.CeilingHeight( room, kit ) ) - 0.5f;
// A wall under the storey above runs to that floor, not its own height.
var roofed = storey > 0f && !freeHead;
// A roof over the wall finishes it; capping anyway would sit under the deck. A link's deck and
// slab both stop on the wall CENTRELINE, so a spanning wall is never finished by them.
var covered = !freeHead && (roofed || Roofed( wall, room, building )) && !room.Spans;
if ( roofed )
{
height = MathF.Max( height, storey );
}
if ( length < 1f || height < 1f )
{
return;
}
var half = thickness * 0.5f;
var join = connections.Wall( wall, room )
.WithThickness( thickness )
.Create();
var chain = new[] { wall.Palette, room.Palette, building.Palette };
// Both faces always emitted; skin derives from Enclosed - every wall is authored Exterior.
var enclosed = connections.Enclosed( wall, room, half );
var cladding = wall.Cladding != WallCladding.None
? ArchSurface.Siding
: wall.Exterior && !enclosed ? ArchSurface.WallExterior : ArchSurface.WallInterior;
var exterior = style.Brush( cladding, chain );
var interior = style.Brush( ArchSurface.WallInterior, chain );
var reveal = style.Brush( ArchSurface.Reveal, chain );
var trim = style.Brush( ArchSurface.Trim, chain );
// A breached archway belongs to the flight that walked through here, so it closes with it.
// A unit a cut swallowed drops out whole - frame, sashes, casing and sill. The hole it stood in is
// the cut's own archway, which covers it, so the wall stays open where the window was.
var openings = wall.Openings
.Where( opening => opening.Width > 0.5f && opening.Height > 0.5f )
.Where( opening => ArchLayerGate.On( opening ) && ArchLayerGate.Owned( opening.OwnerId ) )
.Where( opening => opening.SwallowedBy == 0 || !ArchLayerGate.Owned( opening.SwallowedBy ) )
.OrderBy( opening => opening.Left )
.ToList();
var cuts = openings
.Concat( connections.SharedOpenings( wall, room, thickness ) )
.OrderBy( opening => opening.Left )
.ToList();
var cutIds = plan?.AllCuts().Select( cut => cut.Id ).ToHashSet()
?? new HashSet<int>();
var booleanOpenings = openings.Where( opening => cutIds.Contains( opening.OwnerId ) ).ToList();
var from = join.From;
var to = join.To;
var interiorFrom = join.SkirtFrom;
var interiorTo = join.SkirtTo;
var exteriorFrom = join.StartExtend;
var exteriorTo = join.EndExtend;
// The finished floor, not the slab; opening cuts deliberately keep the structural datum.
var finish = Finish( room, kit );
var modifiers = ArchWallModShape.For( wall, join, height, thickness, kit );
modifiers.AddRange( ArchWallDamage.CornerRecesses( plan, kit, building, room, wall, exteriorFrom, exteriorTo, height, thickness ) );
var bitten = modifiers.Where( shape => shape.Carves ).ToList();
var carvedOutside = bitten.Where( shape => shape.Outside ).ToList();
var carvedInside = bitten.Where( shape => shape.Inside ).ToList();
var proudOutside = modifiers.Where( shape => !shape.Carves && shape.Outside ).ToList();
var proudInside = modifiers.Where( shape => !shape.Carves && shape.Inside ).ToList();
foreach ( var panel in Panels( from, to, height, cuts, join.Landings ) )
{
var innerFrom = MathF.Max( panel.MinX, interiorFrom );
var innerTo = MathF.Min( panel.MaxX, interiorTo );
if ( !wall.SingleSided && innerTo - innerFrom > 0.01f )
{
foreach ( var patch in Faced( panel with { MinX = innerFrom, MaxX = innerTo }, carvedInside ) )
{
canvas.Quad(
new Vector3( patch.MinX, half, patch.MinZ ),
new Vector3( patch.MinX, half, patch.MaxZ ),
new Vector3( patch.MaxX, half, patch.MaxZ ),
new Vector3( patch.MaxX, half, patch.MinZ ),
interior );
}
}
var outerFrom = MathF.Max( panel.MinX, exteriorFrom );
var outerTo = MathF.Min( panel.MaxX, exteriorTo );
if ( outerTo - outerFrom <= 0.01f )
{
continue;
}
foreach ( var course in Banded( panel, wall.Bands, height ) )
{
var field = course.Panel;
var xFrom = MathF.Max( field.MinX, outerFrom );
var xTo = MathF.Min( field.MaxX, outerTo );
if ( xTo - xFrom <= 0.01f )
{
continue;
}
var skin = course.Field is { } role ? style.Brush( role, chain ) : exterior;
foreach ( var patch in Faced( field with { MinX = xFrom, MaxX = xTo }, carvedOutside ) )
{
canvas.Quad(
new Vector3( patch.MinX, -half, patch.MinZ ),
new Vector3( patch.MaxX, -half, patch.MinZ ),
new Vector3( patch.MaxX, -half, patch.MaxZ ),
new Vector3( patch.MinX, -half, patch.MaxZ ),
skin );
if ( course.Field is null )
{
Clad( canvas, patch, half, kit, wall.Cladding, exterior );
}
}
}
}
using ( canvas.Part( ArchPieces.Trims ) )
{
BandCourses( canvas, wall, room, building, plan, kit, join, height, half, proudOutside, style.Brush( ArchSurface.Trim, chain ) );
Mouldings( canvas, wall, room, building, plan, kit, join, height, half, proudOutside, proudInside, style, chain );
Pilasters( canvas, wall, room, building, plan, kit, join, height, half, style.Brush( ArchSurface.Pillar, chain ) );
}
using ( canvas.Part( ArchPieces.Fittings ) )
{
Modifiers( canvas, modifiers, half, style, chain );
ArchDamage.WallCorners( canvas, plan, kit, building, room, wall, exteriorFrom, exteriorTo, height, thickness, style, connections );
}
// The caps close the wall's own box, so they stay on its body - split off, the shell they seal is a
// solid with three open sides and nothing says which part it belongs to.
CapEnds( canvas, height, half, join, reveal, exterior );
CapTop( canvas, wall, room, building, plan, kit, join, covered, height, half, style, chain );
if ( room.Spans )
{
CapFoot( canvas, join, half, style, chain );
}
using ( canvas.Part( ArchPieces.Openings ) )
{
ArchWallOpenings.BooleanReveals( canvas, booleanOpenings, half, height, style.Brush( ArchWallOpenings.BooleanRevealSurface, chain ) );
ArchWallOpenings.BooleanCasing( canvas, booleanOpenings, kit, half, height, trim );
foreach ( var opening in openings.Where( opening => !cutIds.Contains( opening.OwnerId ) ) )
{
// Opening leads the palette chain, so one window can vary without touching the building.
var glazed = opening.IsGlazed;
var glass = new[] { opening.Palette, wall.Palette, room.Palette, building.Palette };
var sill = NeedsSill( opening );
// Lining and architrave follow the unit in the hole; archways have none.
var joinery = opening.Kind switch
{
OpeningKind.Window => style.Brush( ArchSurface.WindowFrame, glass ),
OpeningKind.Door or OpeningKind.DoubleDoor or OpeningKind.Garage => style.Brush( ArchSurface.DoorLeaf, chain ),
_ => (ArchBrush?)null
};
// An archway holds nothing to line: its jambs are the wall's own finish carried through the
// thickness, and a lining skin there bands the passage in another material at the mouth.
var lining = opening.Kind == OpeningKind.Archway ? interior : reveal;
ArchWallOpenings.Reveals( canvas, opening, half, height, sill, joinery ?? lining );
if ( opening.Cased )
{
ArchWallOpenings.Casing( canvas, opening, kit, half, height, joinery ?? trim );
}
if ( sill )
{
ArchWallOpenings.Sill( canvas, opening, kit, half, style.Brush( ArchSurface.Sill, chain ) );
}
ArchWallOpenings.Furniture( canvas, opening, kit, half, height, style, glass );
var saddle = ArchWallOpenings.Threshold( canvas, opening, kit, half,
MathF.Max( finish, Finish( connections.RoomAcross( wall, room, opening.Offset, half ), kit ) ),
style.Brush( ArchSurface.Threshold, chain ) );
if ( glazed )
{
var panes = opening.Breakable ? canvas.Sibling() : canvas;
ArchWindowGen.Build(
canvas, panes, opening, kit, half, height,
style.Brush( ArchSurface.WindowFrame, glass ),
style.Brush( ArchSurface.WindowSash, glass ),
style.Brush( ArchSurface.Glass, glass ) );
if ( opening.Breakable )
{
glazing.Add( new ArchGlassRequest { Opening = opening, Canvas = panes } );
}
}
if ( opening.IsHung )
{
ArchWallOpenings.Leaves( doors, opening, wall, kit, saddle, style.Brush( ArchSurface.DoorLeaf, chain ) );
}
}
}
using ( canvas.Part( ArchPieces.Trims ) )
{
if ( wall.Baseboard )
{
using ( canvas.Part( ArchPieces.Skirting ) )
{
Baseboards( canvas, wall, room, building, plan, join, half, kit, cuts, style.Brush( ArchSurface.Baseboard, chain ),
Skirting( wall, room, building, plan, kit, half, height ), finish );
}
}
if ( wall.Wainscot )
{
Wainscot( canvas, join.SkirtFrom, join.SkirtTo, half, kit, cuts, style.Brush( ArchSurface.Wainscot, chain ), trim, finish );
}
}
}
static bool Roofed( ArchWall wall, ArchRoom room, ArchBuilding building )
{
return ArchOpeningClearance.Covering( building, room, wall ) is not null;
}
static float Finish( ArchRoom room, ArchKit kit )
{
if ( room is null || !room.FloorBoards )
{
return 0f;
}
return MathF.Max( 0f, kit.FloorBoardThickness );
}
static bool NeedsSill( ArchOpening opening )
{
return opening.IsSilled && opening.SillHeight > 0.5f;
}
public readonly struct Panel
{
public float MinX { get; init; }
public float MaxX { get; init; }
public float MinZ { get; init; }
public float MaxZ { get; init; }
}
// Field is the role; null = the wall's own cladding above the top band.
public readonly struct Course
{
public Panel Panel { get; init; }
public ArchSurface? Field { get; init; }
}
// Bands cut the same z split as openings, so fields share one reveal set.
public static IEnumerable<Course> Banded( Panel panel, IEnumerable<ArchWallBand> bands, float wallHeight )
{
var cursor = panel.MinZ;
foreach ( var band in ArchWallTreatments.Ordered( bands, wallHeight ) )
{
var at = band.At( wallHeight );
// Neither skip is an error: a door-head panel sits above the brick line.
if ( at <= cursor + 0.05f )
{
continue;
}
if ( at >= panel.MaxZ - 0.05f )
{
break;
}
yield return new Course
{
Panel = panel with { MinZ = cursor, MaxZ = at },
Field = band.Field
};
cursor = at;
}
if ( panel.MaxZ - cursor > 0.01f )
{
yield return new Course { Panel = panel with { MinZ = cursor, MaxZ = panel.MaxZ }, Field = null };
}
}
public static IEnumerable<Panel> Panels( float from, float to, float height, IReadOnlyList<ArchOpening> openings,
IReadOnlyList<float> splits = null )
{
var cuts = new List<float> { from, to };
foreach ( var opening in openings )
{
cuts.Add( Math.Clamp( opening.Left, from, to ) );
cuts.Add( Math.Clamp( opening.Right, from, to ) );
}
// A junction splits the field like an opening, so the landed-on quad ends exactly there.
foreach ( var split in splits ?? Array.Empty<float>() )
{
if ( split > from + 0.01f && split < to - 0.01f )
{
cuts.Add( split );
}
}
cuts = cuts.Distinct().OrderBy( value => value ).ToList();
for ( var index = 0; index < cuts.Count - 1; index++ )
{
var minX = cuts[index];
var maxX = cuts[index + 1];
if ( maxX - minX < 0.01f )
{
continue;
}
var centre = (minX + maxX) * 0.5f;
var holes = openings
.Where( opening => centre > opening.Left && centre < opening.Right )
.Select( opening => (Bottom: Math.Max( 0f, opening.SillHeight ), Top: Math.Min( height, opening.Top )) )
.Where( hole => hole.Top > hole.Bottom )
.OrderBy( hole => hole.Bottom )
.ToList();
var cursor = 0f;
foreach ( var hole in holes )
{
if ( hole.Bottom - cursor > 0.01f )
{
yield return new Panel { MinX = minX, MaxX = maxX, MinZ = cursor, MaxZ = hole.Bottom };
}
cursor = Math.Max( cursor, hole.Top );
}
if ( height - cursor > 0.01f )
{
yield return new Panel { MinX = minX, MaxX = maxX, MinZ = cursor, MaxZ = height };
}
}
}
static IEnumerable<(float From, float To)> FittingRuns(
ArchPlan plan,
ArchKit kit,
ArchRoom room,
ArchBuilding building,
ArchWall wall,
float from,
float to,
float offset,
float bottom,
float top )
{
var start = wall.Start + wall.Direction * from + wall.Normal * offset;
var finish = wall.Start + wall.Direction * to + wall.Normal * offset;
foreach ( var span in ArchCut.Outside(
plan,
kit,
room.Floor,
building?.Id ?? 0,
start,
finish,
room.BaseHeight + bottom,
room.BaseHeight + top,
ArchCutAffects.WallFittings,
wall.Id ) )
{
yield return (from + (to - from) * span.From, from + (to - from) * span.To);
}
}
static void Run(
ArchMesh canvas,
ArchBandEnd from,
ArchBandEnd to,
ArchWall wall,
ArchRoom room,
ArchBuilding building,
ArchPlan plan,
float half,
float depth,
float height,
ArchKit kit,
ArchBrush brush,
float lift )
{
foreach ( var span in FittingRuns( plan, kit, room, building, wall, from.Inner, to.Inner, half + depth * 0.5f, lift, lift + height ) )
{
var start = EndAt( from, to, span.From, true );
var finish = EndAt( from, to, span.To, false );
ArchBandGen.Run( canvas, start, finish, ArchBandFrame.Local, half, depth, ArchContact.Bury( kit, lift, 1f ), lift + height, brush );
}
}
static ArchBandEnd EndAt( ArchBandEnd from, ArchBandEnd to, float at, bool start )
{
var span = to.Inner - from.Inner;
var amount = span < 0.001f ? 0f : (at - from.Inner) / span;
var original = start ? from : to;
var stop = MathF.Abs( amount - (start ? 0f : 1f) ) < 0.001f ? original.Stop : ArchBandStop.Cut;
var returned = stop == ArchBandStop.Returned ? original.Return : 0f;
return new ArchBandEnd( at, from.Outer + (to.Outer - from.Outer) * amount, stop, returned );
}
}