Editor/Pillar/ArchPillarGen.cs

Editor-side geometry generator for pillars and pilasters. Computes pillar heights, column positions and runs, builds mesh geometry (shafts, caps, spans, pilasters), handles carving when other geometry overlaps, and emits polygons/prisms to an ArchMesh.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly record struct ArchPillarRun( int FromX, int FromY, int ToX, int ToY, bool AlongX );

public readonly record struct ArchPillarColumn( Vector3 At, int X, int Y );

public static class ArchPillarGen
{
	// One answer to how tall a pillar stands: its authored height where it has one, else up to the lowest thing
	// standing over it. The generator, the ghost, the handle and the extents report all resolve the same way, so
	// a part that climbs past its room reads the same to the shaft that builds it and the box that selects it.
	//
	// Level with that soffit rather than lapped into it: the contact pass deletes a face its coplanar neighbour
	// covers whole, so a bite would only bury a surviving face inside the slab.
	public static float Height( ArchPillarPart part, ArchRoom room, ArchKit kit, ArchPlan plan = null )
	{
		if ( part.Height > 0f )
		{
			return part.Height;
		}

		if ( room is null )
		{
			return kit.WallHeight;
		}

		return MathF.Max( 8f, ArchPillarSoffit.Over( plan, kit, room, part ) - part.BaseHeight );
	}

	// Computed once, so the spandrel and the run landing on it cannot disagree.
	public static List<ArchPillarRun> Runs( ArchPillarPart part )
	{
		var runs = new List<ArchPillarRun>();

		if ( part.Span == PillarSpan.None || part.Placement != PillarPlacement.Grid )
		{
			return runs;
		}

		var countX = Math.Max( 1, part.CountX );
		var countY = Math.Max( 1, part.CountY );
		var edge = part.PerimeterOnly;

		if ( part.SpanAlongX )
		{
			for ( var iy = 0; iy < countY; iy++ )
			{
				if ( edge && iy > 0 && iy < countY - 1 )
				{
					continue;
				}

				for ( var ix = 0; ix < countX - 1; ix++ )
				{
					runs.Add( new ArchPillarRun( ix, iy, ix + 1, iy, true ) );
				}
			}
		}

		if ( !part.SpanAlongY )
		{
			return runs;
		}

		for ( var ix = 0; ix < countX; ix++ )
		{
			if ( edge && ix > 0 && ix < countX - 1 )
			{
				continue;
			}

			for ( var iy = 0; iy < countY - 1; iy++ )
			{
				runs.Add( new ArchPillarRun( ix, iy, ix, iy + 1, false ) );
			}
		}

		return runs;
	}

	public static HashSet<(int X, int Y)> Carried( ArchPillarPart part )
	{
		var carried = new HashSet<(int X, int Y)>();

		foreach ( var run in Runs( part ) )
		{
			carried.Add( (run.FromX, run.FromY) );
			carried.Add( (run.ToX, run.ToY) );
		}

		return carried;
	}

	public static void Build(
		ArchMesh canvas,
		ArchPillarPart part,
		ArchRoom room,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		ArchStyle style )
	{
		var chain = new[] { part.Palette, room.Palette, building.Palette };
		var shaft = style.Brush( ArchSurface.Pillar, chain );
		var cap = style.Brush( ArchSurface.PillarCap, chain );

		var height = Height( part, room, kit, plan );

		if ( part.Placement == PillarPlacement.Pilaster )
		{
			Pilaster( canvas, part, room, height, shaft, cap, kit );
			return;
		}

		// A span bears ON the columns, so the shaft stops at the springing.
		var spanning = part.Span != PillarSpan.None && part.Placement == PillarPlacement.Grid;
		var shaftHeight = spanning ? MathF.Max( 8f, height - part.SpanBand ) : height;
		var carried = spanning ? Carried( part ) : new HashSet<(int X, int Y)>();

		// Only a column a run springs off gives up its top to the span.
		foreach ( var column in Columns( part ) )
		{
			var columnHeight = carried.Contains( (column.X, column.Y) ) ? shaftHeight : height;

			Column( canvas, part, column.At, columnHeight, shaft, cap,
				Bites( plan, kit, part, room, building, column.At, columnHeight ) );

			ArchDamage.PillarCorners( canvas, plan, kit, building, room, part, column.At, columnHeight, style );
		}

		if ( spanning )
		{
			using ( canvas.Part( ArchPieces.Span ) )
			{
				Spans( canvas, part, kit, height, cap );
			}
		}
	}

	// A section standing where something else already decided its seat - roof plant on a deck plane, not a column
	// in a room, so there is no wall height to fall back on and its own is the only answer.
	public static void Stand( ArchMesh canvas, ArchPillarPart part, ArchBrush shaft, ArchBrush cap )
	{
		foreach ( var column in Columns( part ) )
		{
			Column( canvas, part, column.At, MathF.Max( 1f, part.Height ), shaft, cap );
		}
	}

	public static List<Vector3> Positions( ArchPillarPart part ) => Columns( part ).Select( column => column.At ).ToList();

	public static List<ArchPillarColumn> Columns( ArchPillarPart part )
	{
		var columns = new List<ArchPillarColumn>();
		var rotation = Rotation.FromYaw( part.Yaw );

		if ( part.Placement != PillarPlacement.Grid )
		{
			columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x, part.Origin.y, part.BaseHeight ), 0, 0 ) );
			return columns;
		}

		var countX = Math.Max( 1, part.CountX );
		var countY = Math.Max( 1, part.CountY );

		for ( var ix = 0; ix < countX; ix++ )
		{
			for ( var iy = 0; iy < countY; iy++ )
			{
				var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
				var world = rotation * local;

				columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x + world.x, part.Origin.y + world.y, part.BaseHeight ), ix, iy ) );
			}
		}

		return columns;
	}

	readonly record struct Course( float Bottom, float Top, Vector2 Lower, Vector2 Upper, bool Dressed );

	// What a Subtract standing over this column takes out of it, in the band the column actually occupies. Answered
	// through ArchCut like every other host's, so ArchLayerOrder decides whether a cut authored before the column
	// reaches it at all.
	public static List<ArchCarveVolume> Bites( ArchPlan plan, ArchKit kit, ArchPillarPart part, ArchRoom room, ArchBuilding building, Vector3 basePoint, float height )
	{
		var bites = new List<ArchCarveVolume>();

		if ( plan is null || room is null )
		{
			return bites;
		}

		var widest = part.Reach;
		var flat = new Vector2( basePoint.x, basePoint.y );
		var footprint = ArchFootprint.Rect( flat - new Vector2( widest, widest ), flat + new Vector2( widest, widest ) );
		var foot = basePoint.z - MathF.Max( 0f, part.FootingBuried );
		var head = basePoint.z + height;

		foreach ( var cut in ArchCut.Over( plan, room.Floor, footprint, building?.Id ?? 0, ArchCutAffects.Pillars, part.Id ) )
		{
			bites.AddRange( ArchCut.Resolve( cut, kit ).Where( volume => ArchCut.Reaches( volume, foot, head ) ) );
		}

		return bites;
	}

	static void Column( ArchMesh canvas, ArchPillarPart part, Vector3 basePoint, float height, ArchBrush shaft, ArchBrush cap, List<ArchCarveVolume> bites = null )
	{
		var courses = Courses( part, basePoint.z, height );

		if ( courses.Count == 0 )
		{
			return;
		}

		var flat = new Vector2( basePoint.x, basePoint.y );

		if ( bites is { Count: > 0 } )
		{
			Carved( canvas, part, flat, courses, bites, shaft, cap );

			return;
		}

		Hulled( canvas, part, flat, courses, shaft, cap );
	}

	// A column nothing carves stays ONE welded hull, which is what keeps a colonnade walkable and what stops a
	// stacked box sealing a face pair at every step.
	static void Hulled( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, ArchBrush shaft, ArchBrush cap )
	{
		var sides = part.Sides;

		using var welding = canvas.Welding();

		// ONE shape for the whole column, not one per course: the hull over every ring closes the inset at a plinth
		// or a capital step, which nothing can stand in anyway, and a colonnade stays walkable because each column
		// is its own hull rather than all of them being one mesh.
		using var solid = canvas.Solid( ArchSolid.Hull( Rings( courses, flat, sides, part.Yaw ) ) );

		canvas.Polygon( Ring( flat, courses[0].Lower, courses[0].Bottom, sides, part.Yaw ), courses[0].Dressed ? cap : shaft, true );

		for ( var index = 0; index < courses.Count; index++ )
		{
			var course = courses[index];
			var brush = course.Dressed ? cap : shaft;

			Faces( canvas, flat, course, sides, part.Yaw, brush );

			if ( index == courses.Count - 1 )
			{
				continue;
			}

			Step( canvas, flat, course, courses[index + 1], sides, part.Yaw, brush );
		}

		var last = courses[^1];

		canvas.Polygon( Ring( flat, last.Upper, last.Top, sides, part.Yaw ), last.Dressed ? cap : shaft );
	}

	// A column a cut reaches has no hull to be, so it goes through the same carve algebra a platform does - one
	// prism per course, so a bite's cheeks share the shaft's own edges. A course that TAPERS comes out straight
	// sided here: the prism algebra has no taper, and the only tapering course is a footing, which is buried.
	static void Carved( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, List<ArchCarveVolume> bites, ArchBrush shaft, ArchBrush cap )
	{
		using var welding = canvas.Welding();

		foreach ( var course in courses )
		{
			var ring = Ring( flat, course.Lower, 0f, part.Sides, part.Yaw )
				.Select( point => new Vector2( point.x, point.y ) )
				.ToList();

			var carve = ArchCarve.Prism( ring, course.Bottom, course.Top ).In( part.Yaw );

			foreach ( var bite in bites )
			{
				carve.Less( bite );
			}

			foreach ( var face in carve.Resolve().Faces )
			{
				canvas.Polygon( face.Points, course.Dressed ? cap : shaft );
			}
		}
	}

	// Every corner the column has, bottom ring to top ring, which is what its hull is taken over.
	static List<Vector3> Rings( List<Course> courses, Vector2 flat, int sides, float yaw )
	{
		var points = new List<Vector3>();

		foreach ( var course in courses )
		{
			points.AddRange( Ring( flat, course.Lower, course.Bottom, sides, yaw ) );
			points.AddRange( Ring( flat, course.Upper, course.Top, sides, yaw ) );
		}

		return points;
	}

	static List<Course> Courses( ArchPillarPart part, float baseHeight, float height )
	{
		var courses = new List<Course>();
		var section = part.Half;

		var plinth = part.Plinth ? MathF.Max( 0f, part.PlinthHeight ) : 0f;
		var capital = part.Capital ? MathF.Max( 0f, part.CapitalHeight ) : 0f;

		var top = baseHeight + height;
		var shaftBottom = baseHeight + plinth;
		var shaftTop = top - capital;

		Footing( part, courses, section, baseHeight );

		if ( plinth > 0.05f )
		{
			var spread = section + new Vector2( part.PlinthOversize, part.PlinthOversize );

			courses.Add( new Course( baseHeight, shaftBottom, spread, spread, true ) );
		}

		if ( shaftTop - shaftBottom > 0.05f )
		{
			courses.Add( new Course( shaftBottom, shaftTop, section, section, false ) );
		}

		if ( capital > 0.05f )
		{
			var spread = section + new Vector2( part.CapitalOversize, part.CapitalOversize );

			courses.Add( new Course( shaftTop, top, spread, spread, true ) );
		}

		return courses;
	}

	static void Footing( ArchPillarPart part, List<Course> courses, Vector2 section, float baseHeight )
	{
		if ( part.Footing == PillarFooting.None )
		{
			return;
		}

		var spread = part.FootingReach;
		var depth = MathF.Max( 1f, part.FootingHeight );
		var buried = MathF.Max( 0f, part.FootingBuried );

		var foot = baseHeight - buried;
		var wide = section + new Vector2( spread, spread );

		switch ( part.Footing )
		{
			case PillarFooting.Bevelled:
				// A third stays square under the taper, or the pad reads as a cone.
				var shelf = foot + depth * 0.34f;

				courses.Add( new Course( foot, shelf, wide, wide, true ) );
				courses.Add( new Course( shelf, foot + depth, wide, section, true ) );
				break;

			case PillarFooting.Stepped:
				var middle = foot + depth * 0.5f;
				var upper = section + new Vector2( spread * 0.5f, spread * 0.5f );

				courses.Add( new Course( foot, middle, wide, wide, true ) );
				courses.Add( new Course( middle, foot + depth, upper, upper, true ) );
				break;

			default:
				courses.Add( new Course( foot, foot + depth, wide, wide, true ) );
				break;
		}
	}

	static void Faces( ArchMesh canvas, Vector2 centre, Course course, int sides, float yaw, ArchBrush brush )
	{
		var lower = Ring( centre, course.Lower, course.Bottom, sides, yaw );
		var upper = Ring( centre, course.Upper, course.Top, sides, yaw );

		for ( var index = 0; index < lower.Length; index++ )
		{
			var next = (index + 1) % lower.Length;

			canvas.Quad( lower[index], lower[next], upper[next], upper[index], brush );
		}
	}

	// Equal sections continue instead - a plain pillar stays six faces.
	static void Step( ArchMesh canvas, Vector2 centre, Course below, Course above, int sides, float yaw, ArchBrush brush )
	{
		if ( MathF.Abs( below.Upper.x - above.Lower.x ) < 0.01f && MathF.Abs( below.Upper.y - above.Lower.y ) < 0.01f )
		{
			return;
		}

		// The two wind opposite ways - the wrong one leaves a hole in the half-edge mesh.
		var stepsIn = below.Upper.x >= above.Lower.x;

		var wide = Ring( centre, stepsIn ? below.Upper : above.Lower, below.Top, sides, yaw );
		var tight = Ring( centre, stepsIn ? above.Lower : below.Upper, below.Top, sides, yaw );

		for ( var index = 0; index < wide.Length; index++ )
		{
			var next = (index + 1) % wide.Length;

			if ( stepsIn )
			{
				canvas.Quad( wide[index], wide[next], tight[next], tight[index], brush );
				continue;
			}

			canvas.Quad( tight[index], tight[next], wide[next], wide[index], brush );
		}
	}

	// The section, whatever number of sides it has: the rectangle by hand so a plain pier keeps its exact
	// corners, and anything rounder through ArchFootprint's own ellipse so a bool's circle and a column's
	// circle are the same loop. Turned about its own centre - a yaw that only marched the grid and left every
	// section square was a turn handle that did nothing at all to a lone column.
	static Vector3[] Ring( Vector2 centre, Vector2 half, float height, int sides, float yaw )
	{
		var corners = sides < 5
			? new[]
			{
				new Vector2( centre.x - half.x, centre.y - half.y ),
				new Vector2( centre.x + half.x, centre.y - half.y ),
				new Vector2( centre.x + half.x, centre.y + half.y ),
				new Vector2( centre.x - half.x, centre.y + half.y )
			}
			: ArchFootprint.Ellipse( centre - half, centre + half, sides ).ToArray();

		var turned = MathF.Abs( yaw % 360f ) < 0.01f ? corners : ArchFootprint.Turned( corners, centre, yaw ).ToArray();

		return turned.Select( point => new Vector3( point.x, point.y, height ) ).ToArray();
	}

	// Seated so its top finishes on the column top, not proud of the capital.
	static void Spans( ArchMesh canvas, ArchPillarPart part, ArchKit kit, float height, ArchBrush brush )
	{
		var rotation = Rotation.FromYaw( part.Yaw );
		var top = part.BaseHeight + height;
		var springing = MathF.Max( part.BaseHeight + 8f, top - part.SpanBand );
		var half = part.Half;
		var lap = ArchContact.Bite( kit );

		foreach ( var run in Runs( part ) )
		{
			var inset = (run.AlongX ? half.x : half.y) - lap;

			Connect( canvas, part,
				Point( part, rotation, run.FromX, run.FromY ), Point( part, rotation, run.ToX, run.ToY ),
				run.AlongX, springing, top, inset, brush );
		}

		// Laps a bite down into the shaft; flush leaves cap and block in one plane.
		foreach ( var (ix, iy) in Carried( part ) )
		{
			var centre = Point( part, rotation, ix, iy );

			canvas.Box(
				new Vector3( centre.x - half.x, centre.y - half.y, ArchContact.Bury( kit, springing, 1f ) ),
				new Vector3( centre.x + half.x, centre.y + half.y, top ),
				brush );
		}
	}

	static Vector2 Point( ArchPillarPart part, Rotation rotation, int ix, int iy )
	{
		var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
		var world = rotation * local;

		return new Vector2( part.Origin.x + world.x, part.Origin.y + world.y );
	}

	// Inset is the pier's half-section less a bite, so the run runs INTO the spandrel. The solid itself comes off
	// ArchSpanGen, which is what makes an arcade and a hand-picked arch the same geometry.
	static void Connect( ArchMesh canvas, ArchPillarPart part, Vector2 from, Vector2 to, bool alongX, float springing, float top, float inset, ArchBrush brush )
	{
		ArchSpanGen.Build( canvas, new ArchSpanRun
		{
			From = from,
			To = to,
			Springing = springing,
			Top = top,
			Thickness = part.SpanThickness( alongX ),
			InsetFrom = inset,
			InsetTo = inset,
			Segments = part.ArchSegments,
			Ring = part.ArchRing,
			Form = part.Span
		}, brush );
	}

	static void Pilaster( ArchMesh canvas, ArchPillarPart part, ArchRoom room, float height, ArchBrush shaft, ArchBrush cap, ArchKit kit )
	{
		var wall = room.Walls.Find( candidate => candidate.Id == part.WallId ) ?? room.Walls.FirstOrDefault();

		if ( wall is null )
		{
			return;
		}

		var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
		var half = thickness * 0.5f;
		var halfWidth = part.PilasterWidth * 0.5f;
		var offset = Math.Clamp( part.WallOffset, halfWidth, Math.Max( halfWidth, wall.Length - halfWidth ) );

		var direction = wall.Direction;
		var normal = wall.Normal;
		var centre = wall.PointAt( offset );

		Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, false );

		if ( part.PilasterBothFaces )
		{
			Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, true );
		}
	}

	static void Face(
		ArchMesh canvas,
		Vector2 centre,
		Vector2 direction,
		Vector2 normal,
		float halfWidth,
		float wallHalf,
		ArchPillarPart part,
		float height,
		ArchBrush shaft,
		ArchBrush cap,
		bool inner )
	{
		var sign = inner ? -1f : 1f;
		var near = wallHalf * sign;
		var far = (wallHalf + part.PilasterProtrusion) * sign;

		var bottom = part.BaseHeight;
		var plinth = part.Plinth ? Math.Max( 0f, part.PlinthHeight ) : 0f;
		var capital = part.Capital ? Math.Max( 0f, part.CapitalHeight ) : 0f;

		Slab( canvas, centre, direction, normal, halfWidth, near, far, bottom + plinth, bottom + height - capital, shaft );

		if ( plinth > 0.05f )
		{
			Slab( canvas, centre, direction, normal, halfWidth + part.PlinthOversize, near, far + part.PlinthOversize * sign, bottom, bottom + plinth, cap );
		}

		if ( capital > 0.05f )
		{
			Slab( canvas, centre, direction, normal, halfWidth + part.CapitalOversize, near, far + part.CapitalOversize * sign, bottom + height - capital, bottom + height, cap );
		}
	}

	static void Slab(
		ArchMesh canvas,
		Vector2 centre,
		Vector2 direction,
		Vector2 normal,
		float halfWidth,
		float near,
		float far,
		float bottom,
		float top,
		ArchBrush brush )
	{
		if ( top - bottom < 0.05f )
		{
			return;
		}

		var a = centre - direction * halfWidth + normal * near;
		var b = centre + direction * halfWidth + normal * near;
		var c = centre + direction * halfWidth + normal * far;
		var d = centre - direction * halfWidth + normal * far;

		var lower = new List<Vector3>
		{
			new( a.x, a.y, bottom ),
			new( b.x, b.y, bottom ),
			new( c.x, c.y, bottom ),
			new( d.x, d.y, bottom )
		};

		var upper = new List<Vector3>();

		foreach ( var point in lower )
		{
			upper.Add( point.WithZ( top ) );
		}

		canvas.Prism( lower, upper, brush );
	}
}