Editor/Wall/ArchWallMods.cs
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, float wallHeight ) {
var crossing = proud.Where( shape => shape.Spans( bottom, top, wallHeight ) ).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]);
}
}
}
// The skirt hanging off a canopy's front edge - a straight band with a row of tabs under it, each tab
// narrowing to read as a scallop. Counted off the width by ArchDivide, so no awning carries a tab count,
// and cut as tapered slabs rather than half-rounds: at the distance a shopfront is seen from, the notch
// between two tabs is the whole of what a scallop IS.
static void Valance( ArchMesh canvas, ArchWallModBlock block, float face, float sign, ArchKit kit, ArchBrush brush ) {
var drop = MathF.Max( 0f, kit.AwningValance );
var thickness = MathF.Max( 0.3f, kit.AwningThickness );
var width = block.To - block.From;
if ( drop < 1f || width < 2f ) {
return;
}
var front = face + sign * block.Foot;
var back = front - sign * thickness;
var band = drop * 0.45f;
var shoulder = block.Bottom - band;
var hem = block.Bottom - drop;
canvas.Box(
new Vector3( block.From, MathF.Min( front, back ), shoulder ),
new Vector3( block.To, MathF.Max( front, back ), block.Bottom ),
brush );
var tabs = ArchDivide.AtMost( width, MathF.Max( 2f, kit.AwningScallop ) );
for ( var tab = 0; tab < tabs.Count; tab++ ) {
var from = block.From + tabs.Step * tab;
var to = from + tabs.Step;
var inset = tabs.Step * 0.22f;
canvas.Prism(
Plate( from + inset, to - inset, front, back, hem ),
Plate( from, to, front, back, shoulder ),
brush );
}
}
static void Modifiers( ArchMesh canvas, IReadOnlyList<ArchWallModShape> shapes, float half, ArchKit kit, 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, kit, brush );
if ( shape.Part.Kind.Falls() ) {
Valance( canvas, block, half * face, face, kit, 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.
// The back is LEFT on the face it stands proud of: the contact pass already deletes a back its wall face covers
// whole, and biting in instead keeps that face, drives the block into the wall and deepens every corner overlap.
static void Proud( ArchMesh canvas, ArchWallModBlock block, float face, float sign, ArchKit kit, ArchBrush brush ) {
var wallThickness = MathF.Abs( face ) * 2f;
var projection = MathF.Max( MathF.Abs( block.Foot ), MathF.Abs( block.Head ) );
var sink = ArchLap.Bite( kit ) * (2f + MathF.Min( 1f, projection / MathF.Max( ArchGridService.FinestSize, wallThickness ) ));
var back = face - sign * sink;
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( back, foot ), block.Bottom ),
new Vector3( block.To, MathF.Max( back, foot ), block.Top ),
brush );
return;
}
canvas.Prism(
Plate( block.From, block.To, back, foot, block.Bottom ),
Plate( block.From, block.To, back, 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 );
}
}
}