Editor-side static helper for generating wall geometry and modifiers. It breaks wall panels into faced segments, computes stretches and returns for proud/proud and recessed modifiers, and emits mesh primitives (boxes, prisms, quads) to an ArchMesh using ArchBrush/ArchStyle data.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchWallGen
{
// The outer field minus what a carved modifier takes out of it. The inner face never sees them:
// a recess is a bite out of ONE elevation, not a hole through the wall.
public static IEnumerable<Panel> Faced( Panel field, IReadOnlyList<ArchWallModShape> carved )
{
if ( carved.Count == 0 )
{
yield return field;
yield break;
}
var cuts = new List<float> { field.MinX, field.MaxX };
foreach ( var shape in carved )
{
cuts.Add( Math.Clamp( shape.From, field.MinX, field.MaxX ) );
cuts.Add( Math.Clamp( shape.To, field.MinX, field.MaxX ) );
}
foreach ( var column in Stretches( cuts ) )
{
var centre = (column.From + column.To) * 0.5f;
var holes = carved
.Where( shape => centre > shape.From && centre < shape.To )
.Select( shape => (Bottom: MathF.Max( field.MinZ, shape.Bottom ), Top: MathF.Min( field.MaxZ, shape.Top )) )
.Where( hole => hole.Top > hole.Bottom )
.OrderBy( hole => hole.Bottom )
.ToList();
var cursor = field.MinZ;
foreach ( var hole in holes )
{
if ( hole.Bottom - cursor > 0.01f )
{
yield return field with { MinX = column.From, MaxX = column.To, MinZ = cursor, MaxZ = hole.Bottom };
}
cursor = MathF.Max( cursor, hole.Top );
}
if ( field.MaxZ - cursor > 0.01f )
{
yield return field with { MinX = column.From, MaxX = column.To, MinZ = cursor, MaxZ = field.MaxZ };
}
}
}
// A band meeting a proud modifier RETURNS over it rather than driving through it: the run comes
// back as stretches, each carrying the depth of whatever it crosses.
public static IEnumerable<(float From, float To, float Depth)> Returns( float from, float to, float depth,
IReadOnlyList<ArchWallModShape> proud, float bottom, float top )
{
var crossing = proud.Where( shape => shape.Spans( bottom, top ) ).ToList();
if ( crossing.Count == 0 || to - from < 0.5f )
{
yield return (from, to, depth);
yield break;
}
var cuts = new List<float> { from, to };
foreach ( var shape in crossing )
{
cuts.Add( Math.Clamp( shape.From, from, to ) );
cuts.Add( Math.Clamp( shape.To, from, to ) );
}
foreach ( var stretch in Stretches( cuts ) )
{
var centre = (stretch.From + stretch.To) * 0.5f;
var standing = crossing
.Where( shape => centre > shape.From && centre < shape.To )
.Select( shape => shape.Depth )
.DefaultIfEmpty( 0f )
.Max();
yield return (stretch.From, stretch.To, depth + standing);
}
}
static IEnumerable<(float From, float To)> Stretches( List<float> cuts )
{
var ordered = cuts.Distinct().OrderBy( value => value ).ToList();
for ( var index = 0; index < ordered.Count - 1; index++ )
{
if ( ordered[index + 1] - ordered[index] > 0.01f )
{
yield return (ordered[index], ordered[index + 1]);
}
}
}
static void Modifiers( ArchMesh canvas, IReadOnlyList<ArchWallModShape> shapes, float half, ArchStyle style, ArchPalette[] chain )
{
foreach ( var shape in shapes )
{
// The modifier leads its own chain, so one pilaster can vary without touching the wall.
var brush = style.Brush( shape.Part.Field, new[] { shape.Part.Palette }.Concat( chain ).ToArray() );
// Whether this kind is a row of its own is the kinds table's answer; a null name is the wall's fittings.
using ( canvas.Part( shape.Part.Kind.Piece() ) )
{
foreach ( var face in shape.Faces() )
{
if ( shape.Carves )
{
Recess( canvas, shape, half * face, face, brush );
continue;
}
foreach ( var block in shape.Blocks )
{
Proud( canvas, block, half * face, face, brush );
}
}
}
}
}
// Boxed off explicit local coordinates, never Beam: Beam measures its offsets along
// ArchRegion.Outward, which is the negative Y side, so a block handed the face it names comes out
// standing on the other elevation.
static void Proud( ArchMesh canvas, ArchWallModBlock block, float face, float sign, ArchBrush brush )
{
var foot = face + sign * block.Foot;
var head = face + sign * block.Head;
if ( MathF.Abs( block.Head - block.Foot ) < 0.05f )
{
canvas.Box(
new Vector3( block.From, MathF.Min( face, foot ), block.Bottom ),
new Vector3( block.To, MathF.Max( face, foot ), block.Top ),
brush );
return;
}
canvas.Prism(
Plate( block.From, block.To, face, foot, block.Bottom ),
Plate( block.From, block.To, face, head, block.Top ),
brush );
}
static List<Vector3> Plate( float from, float to, float near, float far, float at )
{
return new List<Vector3>
{
new( from, near, at ),
new( to, near, at ),
new( to, far, at ),
new( from, far, at )
};
}
// The back is RETRACTED inside the thickness, never landing on the far face; the returns line the
// bite the field was cut out of, exactly as an opening's reveals line a hole. The inside face is
// the same box mirrored, so its windings run the other way round.
static void Recess( ArchMesh canvas, ArchWallModShape shape, float face, float sign, ArchBrush brush )
{
var back = face - sign * shape.Depth;
var from = sign < 0f ? shape.From : shape.To;
var to = sign < 0f ? shape.To : shape.From;
var bottom = shape.Bottom;
var top = shape.Top;
if ( shape.Sealed )
{
var opposite = -face + sign * 0.5f;
canvas.Box(
new Vector3( MathF.Min( from, to ), MathF.Min( back, opposite ), bottom ),
new Vector3( MathF.Max( from, to ), MathF.Max( back, opposite ), top ),
brush );
return;
}
canvas.Quad(
new Vector3( from, back, bottom ),
new Vector3( to, back, bottom ),
new Vector3( to, back, top ),
new Vector3( from, back, top ),
brush );
canvas.Quad(
new Vector3( from, face, bottom ),
new Vector3( from, back, bottom ),
new Vector3( from, back, top ),
new Vector3( from, face, top ),
brush );
canvas.Quad(
new Vector3( to, back, bottom ),
new Vector3( to, face, bottom ),
new Vector3( to, face, top ),
new Vector3( to, back, top ),
brush );
canvas.Quad(
new Vector3( from, back, top ),
new Vector3( to, back, top ),
new Vector3( to, face, top ),
new Vector3( from, face, top ),
brush );
// A recess reaching the ground has the slab under it; a soffit there would only z-fight.
if ( bottom > 0.01f )
{
canvas.Quad(
new Vector3( from, face, bottom ),
new Vector3( to, face, bottom ),
new Vector3( to, back, bottom ),
new Vector3( from, back, bottom ),
brush );
}
}
}