Editor/Wall/ArchWallMods.cs

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.

File AccessNative Interop
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 );
		}
	}
}