Editor/Geometry/ArchMeshSweep.cs

Mesh generation utilities for architectural geometry. It builds swept solid, tube and skin meshes from rows of 3D stations and framing info, applies brushes/UVs, handles carved cut-volumes, ribs, caps, undersides and seams.

File AccessNative Interop
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// One row per station, same column count, left to right; the section at a station is built by the caller.
// Every face carries AUTHORED texcoords off the developed surface: a projected mapping cannot tile a strip that bends.
// `paint` re-skins top faces: a crossover is the pavement itself in another material, not a slab laid over it.
// `soffit` names a shared underside outright: a deck's verge starts a kerb-height lower, so depth cannot say it.
// Cuts split the rows first (ArchMeshSweep.Ribs), so no cell straddles a boundary and the test below is exact.
public static partial class ArchMeshSweep
{
	public static void Solid(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<ArchFrame> frames,
		float depth,
		IReadOnlyList<ArchBrush> tops,
		ArchBrush side,
		bool closed,
		Func<int, int, ArchBrush?> paint = null,
		Func<int, float?> soffit = null,
		IReadOnlyList<ArchCarveVolume> carves = null )
	{
		if ( rows.Count < 2 || rows[0].Length < 2 )
		{
			return;
		}

		// A rib at every cut boundary FIRST: a cell that straddles one can only go whole or stay whole.
		var ribbed = Ribbed( rows, carves );
		var grid = ribbed.Rows;
		var across = ribbed.Across;
		var bases = Bases( grid, depth, soffit );
		var columns = ribbed.Columns;
		var segments = closed ? grid.Count : grid.Count - 1;
		var carved = Carved( grid, carves, closed );

		bool Taken( int row, int column ) => carved.Contains( (row, column) );

		// The section's own gap, so a brush list and a paving callback are never shifted by an inserted rib.
		int Gap( int column ) => ribbed.Gap[column];

		for ( var row = 0; row < segments; row++ )
		{
			var next = (row + 1) % grid.Count;
			var along = frames[row].Along;
			var station = frames[row].Distance;
			var ahead = next == 0 ? station + (grid[next][0] - grid[row][0]).Length : frames[next].Distance;

			for ( var column = 0; column < columns; column++ )
			{
				if ( Taken( row, column ) )
				{
					Reveal( canvas, grid, bases, Taken, row, next, column, segments, columns, closed, along, station, ahead, side );

					continue;
				}

				canvas.Ribbon( grid[row][column], grid[row][column + 1], grid[next][column + 1], grid[next][column],
					along, paint?.Invoke( row, Gap( column ) ) ?? Top( tops, Gap( column ) ),
					Cell( across, row, next, column, station, ahead ) );
			}

			// A flank takes its own strip's top brush, or the back of a pavement comes out in kerb stone.
			if ( !Taken( row, 0 ) )
			{
				Flank( canvas, grid, bases, row, next, 0, along, station, ahead, Top( tops, Gap( 0 ) ), false );
			}

			if ( !Taken( row, columns - 1 ) )
			{
				Flank( canvas, grid, bases, row, next, columns, along, station, ahead, Top( tops, Gap( columns - 1 ) ), true );
			}

			Underside( canvas, grid, bases, across, Taken, row, next, columns, along, station, ahead, side );
		}

		if ( closed )
		{
			return;
		}

		Cap( canvas, grid[0], bases[0], Taken, 0, side, false );
		Cap( canvas, grid[^1], bases[^1], Taken, segments - 1, side, true );
	}

	// WHICH CELLS of a swept grid a set of cut volumes takes out - the one answer, read by the road's own strips and
	// by a bore's lining. A cell goes when its own middle stands inside a cut, which is exact only on a grid whose
	// cells do not straddle the cut's boundary: along the run the cut's corners pin their own stations, and across
	// it the ribs are split - by Ribbed for a strip, by ArchTunnelProfile's heads for a lining.
	public static HashSet<(int Row, int Column)> Carved( IReadOnlyList<Vector3[]> rows, IReadOnlyList<ArchCarveVolume> volumes, bool closed = false )
	{
		var carved = new HashSet<(int Row, int Column)>();

		if ( volumes is not { Count: > 0 } || rows is not { Count: > 1 } )
		{
			return carved;
		}

		var segments = closed ? rows.Count : rows.Count - 1;

		for ( var row = 0; row < segments; row++ )
		{
			var next = (row + 1) % rows.Count;

			for ( var column = 0; column + 1 < rows[row].Length; column++ )
			{
				var middle = (rows[row][column] + rows[row][column + 1] + rows[next][column + 1] + rows[next][column]) * 0.25f;

				foreach ( var volume in volumes )
				{
					if ( Takes( volume, middle ) )
					{
						carved.Add( (row, column) );

						break;
					}
				}
			}
		}

		return carved;
	}

	static bool Takes( ArchCarveVolume volume, Vector3 point )
	{
		return volume.Covers( new Vector2( point.x, point.y ) ) && Banded( volume, point );
	}

	static bool Banded( ArchCarveVolume volume, Vector3 point )
	{
		var flat = new Vector2( point.x, point.y );

		return point.z > volume.Floor.At( flat ) && point.z < volume.Ceiling.At( flat );
	}

	// A cell's four corners each sit where they really are on the developed sheet: with no cut in the run the two
	// rows quote one grid and this is ArchWeave.Cell, and where a cut split them the rib is quoted per row, so the
	// two cells sharing it still quote it identically and the tiling cannot step.
	static ArchWeave Cell( IReadOnlyList<float[]> across, int row, int next, int column, float station, float ahead )
	{
		return new ArchWeave
		{
			A = new Vector2( station, across[row][column] ),
			B = new Vector2( station, across[row][column + 1] ),
			C = new Vector2( ahead, across[next][column + 1] ),
			D = new Vector2( ahead, across[next][column] )
		};
	}

	// What a cut leaves behind: every boundary between a cell that went and one that stayed is a face of the hole.
	// A boundary against no neighbour at all is the strip's own outside, which was never there to close.
	static void Reveal(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<float> bases,
		Func<int, int, bool> carved,
		int row,
		int next,
		int column,
		int segments,
		int columns,
		bool closed,
		Vector3 along,
		float station,
		float ahead,
		ArchBrush brush )
	{
		if ( Stands( carved, row, column - 1, segments, columns ) )
		{
			Flank( canvas, rows, bases, row, next, column, along, station, ahead, brush, true );
		}

		if ( Stands( carved, row, column + 1, segments, columns ) )
		{
			Flank( canvas, rows, bases, row, next, column + 1, along, station, ahead, brush, false );
		}

		if ( Stands( carved, Behind( row, segments, closed ), column, segments, columns ) )
		{
			Rib( canvas, rows[row], bases[row], column, brush, true );
		}

		if ( Stands( carved, Ahead( row, segments, closed ), column, segments, columns ) )
		{
			Rib( canvas, rows[next], bases[next], column, brush, false );
		}
	}

	static int Behind( int row, int segments, bool closed ) => closed ? (row + segments - 1) % segments : row - 1;

	static int Ahead( int row, int segments, bool closed ) => closed ? (row + 1) % segments : row + 1;

	// A neighbour that is there and stayed. Off the end of the grid is not standing material: that boundary is the
	// strip's own outside, which was never there to close.
	static bool Stands( Func<int, int, bool> carved, int row, int column, int segments, int columns )
	{
		return row >= 0 && row < segments && column >= 0 && column < columns && carved?.Invoke( row, column ) != true;
	}

	// One quad per row across the whole width, which is what a run with nothing taken out of it wants. A row a cut
	// reached is split cell by cell instead - the hole has to be open from underneath as well as from above.
	static void Underside(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<float> bases,
		IReadOnlyList<float[]> across,
		Func<int, int, bool> carved,
		int row,
		int next,
		int columns,
		Vector3 along,
		float station,
		float ahead,
		ArchBrush brush )
	{
		// The last column is a section rib whatever a cut inserted, so the run's own width is row-independent.
		var width = across[row][columns];

		if ( !Opened( carved, row, columns ) )
		{
			canvas.Ribbon(
				Base( rows[row][columns], bases[row] ),
				Base( rows[row][0], bases[row] ),
				Base( rows[next][0], bases[next] ),
				Base( rows[next][columns], bases[next] ),
				along, brush, ArchWeave.Cell( station, ahead, 0f, width ) );

			return;
		}

		for ( var column = 0; column < columns; column++ )
		{
			if ( carved( row, column ) )
			{
				continue;
			}

			// Quoted back across, like the whole-width quad it replaces, or the soffit's courses mirror where a row splits.
			canvas.Ribbon(
				Base( rows[row][column + 1], bases[row] ),
				Base( rows[row][column], bases[row] ),
				Base( rows[next][column], bases[next] ),
				Base( rows[next][column + 1], bases[next] ),
				along, brush, new ArchWeave
				{
					A = new Vector2( station, width - across[row][column + 1] ),
					B = new Vector2( station, width - across[row][column] ),
					C = new Vector2( ahead, width - across[next][column] ),
					D = new Vector2( ahead, width - across[next][column + 1] )
				} );
		}
	}

	static bool Opened( Func<int, int, bool> carved, int row, int columns )
	{
		for ( var column = 0; column < columns; column++ )
		{
			if ( carved( row, column ) )
			{
				return true;
			}
		}

		return false;
	}

	// One brush per column gap (kerb band vs pavement); a short list runs out on its last entry.
	static ArchBrush Top( IReadOnlyList<ArchBrush> tops, int column )
	{
		return tops.Count == 0 ? default : tops[Math.Min( column, tops.Count - 1 )];
	}

	static void Flank( ArchMesh canvas, IReadOnlyList<Vector3[]> rows, IReadOnlyList<float> bases, int row, int next, int column, Vector3 along, float station, float ahead, ArchBrush brush, bool right )
	{
		var nearTop = rows[row][column];
		var farTop = rows[next][column];
		var nearBase = Base( nearTop, bases[row] );
		var farBase = Base( farTop, bases[next] );

		var near = nearTop.z - bases[row];
		var far = farTop.z - bases[next];

		if ( right )
		{
			canvas.Ribbon( nearTop, nearBase, farBase, farTop, along, brush, new ArchWeave
			{
				A = new Vector2( station, 0f ),
				B = new Vector2( station, near ),
				C = new Vector2( ahead, far ),
				D = new Vector2( ahead, 0f )
			} );

			return;
		}

		canvas.Ribbon( nearTop, farTop, farBase, nearBase, along, brush, new ArchWeave
		{
			A = new Vector2( station, 0f ),
			B = new Vector2( ahead, 0f ),
			C = new Vector2( ahead, far ),
			D = new Vector2( station, near )
		} );
	}

	// ONE grid serves the whole run, taken off the widest row: measured per row, camber and fall drift and the courses wander.
	static float[] Developed( IReadOnlyList<Vector3[]> rows )
	{
		var widest = rows[0];
		var reach = 0f;

		foreach ( var row in rows )
		{
			var width = Across( row );

			if ( width[^1] <= reach ) continue;

			reach = width[^1];
			widest = row;
		}

		return Across( widest );
	}

	// Every ring measured on itself. A tube has no widest row to take one grid off: its two skins stand on
	// different arcs by construction, and on a bend each station's is its own.
	static List<float[]> Arcs( IReadOnlyList<Vector3[]> rows )
	{
		var arcs = new List<float[]>();

		foreach ( var row in rows )
		{
			arcs.Add( Across( row ) );
		}

		return arcs;
	}

	static float[] Across( IReadOnlyList<Vector3> row )
	{
		var across = new float[row.Count];

		for ( var index = 1; index < row.Count; index++ )
		{
			across[index] = across[index - 1] + (row[index] - row[index - 1]).Length;
		}

		return across;
	}

	static void Cap( ArchMesh canvas, IReadOnlyList<Vector3> row, float bottom, Func<int, int, bool> carved, int band, ArchBrush brush, bool end )
	{
		for ( var column = 0; column < row.Count - 1; column++ )
		{
			if ( carved( band, column ) )
			{
				continue;
			}

			Rib( canvas, row, bottom, column, brush, end );
		}
	}

	// One cell of a rib, standing down to the underside: the run's own end, and every face of a hole that runs
	// across the strip rather than along it.
	static void Rib( ArchMesh canvas, IReadOnlyList<Vector3> row, float bottom, int column, ArchBrush brush, bool forward )
	{
		var near = row[column];
		var far = row[column + 1];

		if ( forward )
		{
			canvas.Quad( near, far, Base( far, bottom ), Base( near, bottom ), brush );

			return;
		}

		canvas.Quad( far, near, Base( near, bottom ), Base( far, bottom ), brush );
	}

	// One level underside per station, or the soffit mirrors its own camber; a named soffit still clears the section above.
	static List<float> Bases( IReadOnlyList<Vector3[]> rows, float depth, Func<int, float?> soffit )
	{
		var bases = new List<float>();

		for ( var row = 0; row < rows.Count; row++ )
		{
			var lowest = float.MaxValue;

			foreach ( var point in rows[row] )
			{
				lowest = MathF.Min( lowest, point.z );
			}

			var named = soffit?.Invoke( row );

			bases.Add( named.HasValue
				? MathF.Min( named.Value, lowest - 0.5f )
				: lowest - MathF.Max( 0.5f, depth ) );
		}

		return bases;
	}

	static Vector3 Base( Vector3 point, float bottom ) => point.WithZ( bottom );

	// A bore: lining thickness runs along the profile's normal, not downward, so depth cannot say it.
	// Rings wind right to left: wound the other way the inner surface comes out inside out.
	// `skip` drops a cell from both surfaces and the two skins are bridged round what is left, so an opening in a
	// lining shows the thickness it was built with.
	public static void Tube(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> inner,
		IReadOnlyList<Vector3[]> outer,
		IReadOnlyList<ArchFrame> frames,
		ArchBrush boreFace,
		ArchBrush shellFace,
		Func<int, int, bool> skip = null )
	{
		if ( inner.Count < 2 || inner.Count != outer.Count || inner[0].Length < 2 || outer[0].Length != inner[0].Length )
		{
			return;
		}

		// Each ring's OWN arc. One arc quoted for every station stretches the courses wherever the section moves,
		// and on a bend the outside of the tube travels farther than the inside - the same reason a strip authors
		// its texcoords at all. Two cells sharing a rib still quote that rib identically, so nothing steps.
		var developed = Arcs( inner );
		var shell = Arcs( outer );

		var columns = inner[0].Length - 1;
		var segments = inner.Count - 1;

		for ( var row = 0; row < segments; row++ )
		{
			var next = row + 1;
			var along = frames[row].Along;
			var station = frames[row].Distance;
			var ahead = frames[next].Distance;

			for ( var column = 0; column < columns; column++ )
			{
				if ( skip?.Invoke( row, column ) == true )
				{
					// A lining is TWO skins, so what a cut takes out of it has a thickness to show: every boundary
					// between a cell that went and one that stayed is bridged bore face to shell, which is what
					// makes an opening in a bore read as a lined mouth rather than a slot cut in paper. A chamber
					// standing in the hole then meets a wall rather than an open edge.
					Bridged( canvas, inner, outer, frames, developed, skip, row, next, column, segments, columns, shellFace );

					continue;
				}

				canvas.Ribbon( inner[row][column], inner[row][column + 1], inner[next][column + 1], inner[next][column],
					along, boreFace, Cell( developed, row, next, column, station, ahead ) );

				// Wound back across, so its weave is quoted back across with it and off the shell's OWN arc:
				// the bore cell's weave run the other way mirrors every course on the outside of the lining.
				canvas.Ribbon( outer[row][column + 1], outer[row][column], outer[next][column], outer[next][column + 1],
					along, shellFace, new ArchWeave
					{
						A = new Vector2( station, shell[row][column + 1] ),
						B = new Vector2( station, shell[row][column] ),
						C = new Vector2( ahead, shell[next][column] ),
						D = new Vector2( ahead, shell[next][column + 1] )
					} );
			}
		}

		Ring( canvas, inner[0], outer[0], developed[0], shellFace, false, skip, 0 );
		Ring( canvas, inner[^1], outer[^1], developed[^1], shellFace, true, skip, segments - 1 );

		// The open profile's feet are seams down the run: left as corners they leave an open edge mouth to mouth.
		Seam( canvas, inner, outer, frames, 0, shellFace, skip );
		Seam( canvas, inner, outer, frames, columns, shellFace, skip );
	}

	// The faces of an opening in a lining, wound exactly as the profile's own feet and end rings are - so the
	// mouth of a cross-passage is the same solid edge the bore already shows where it stops.
	static void Bridged(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> inner,
		IReadOnlyList<Vector3[]> outer,
		IReadOnlyList<ArchFrame> frames,
		IReadOnlyList<float[]> developed,
		Func<int, int, bool> skip,
		int row,
		int next,
		int column,
		int segments,
		int columns,
		ArchBrush brush )
	{
		if ( Stands( skip, row, column - 1, segments, columns ) )
		{
			Foot( canvas, inner, outer, frames, row, column, brush, false );
		}

		if ( Stands( skip, row, column + 1, segments, columns ) )
		{
			Foot( canvas, inner, outer, frames, row, column + 1, brush, true );
		}

		if ( Stands( skip, row - 1, column, segments, columns ) )
		{
			Annulus( canvas, inner[row], outer[row], developed[row], column, brush, true );
		}

		if ( Stands( skip, row + 1, column, segments, columns ) )
		{
			Annulus( canvas, inner[next], outer[next], developed[next], column, brush, false );
		}
	}

	// A seam dies with the cell above it, or the lining's bottom edge runs straight across the opening.
	static void Seam( ArchMesh canvas, IReadOnlyList<Vector3[]> inner, IReadOnlyList<Vector3[]> outer, IReadOnlyList<ArchFrame> frames, int column, ArchBrush brush, Func<int, int, bool> skip )
	{
		var right = column == 0;
		var cell = right ? 0 : column - 1;

		for ( var row = 0; row < inner.Count - 1; row++ )
		{
			if ( skip?.Invoke( row, cell ) == true )
			{
				continue;
			}

			Foot( canvas, inner, outer, frames, row, column, brush, right );
		}
	}

	// One station of a seam down the run: the lining's own thickness, standing across the gap between its skins.
	// Wound off the side it is on, or a foot faces up into the solid it closes - and the weave is quoted in that
	// winding, or the two feet of one lining tile opposite ways.
	static void Foot(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> inner,
		IReadOnlyList<Vector3[]> outer,
		IReadOnlyList<ArchFrame> frames,
		int row,
		int column,
		ArchBrush brush,
		bool right )
	{
		var next = row + 1;
		var station = frames[row].Distance;
		var ahead = frames[next].Distance;
		// Each END its own thickness: a bore whose section moves down the run carries the lining's depth with it, and
		// one number quoted to both ends of a tapering foot steps against the station beside it.
		var here = (outer[row][column] - inner[row][column]).Length;
		var there = (outer[next][column] - inner[next][column]).Length;

		if ( right )
		{
			canvas.Ribbon( outer[row][column], inner[row][column], inner[next][column], outer[next][column],
				frames[row].Along, brush, new ArchWeave
				{
					A = new Vector2( station, here ),
					B = new Vector2( station, 0f ),
					C = new Vector2( ahead, 0f ),
					D = new Vector2( ahead, there )
				} );

			return;
		}

		canvas.Ribbon( inner[row][column], outer[row][column], outer[next][column], inner[next][column],
			frames[row].Along, brush, new ArchWeave
			{
				A = new Vector2( station, 0f ),
				B = new Vector2( station, here ),
				C = new Vector2( ahead, there ),
				D = new Vector2( ahead, 0f )
			} );
	}

	// Closes the lining end; buried in a portal headwall wherever there is one.
	static void Ring(
		ArchMesh canvas,
		IReadOnlyList<Vector3> inner,
		IReadOnlyList<Vector3> outer,
		IReadOnlyList<float> developed,
		ArchBrush brush,
		bool end,
		Func<int, int, bool> skip,
		int band )
	{
		for ( var column = 0; column < inner.Count - 1; column++ )
		{
			if ( skip?.Invoke( band, column ) == true )
			{
				continue;
			}

			Annulus( canvas, inner, outer, developed, column, brush, end );
		}
	}

	// One cell of that ring, which is also every face of an opening that runs ACROSS the lining rather than
	// along it.
	//
	// ONE arc position per radial edge, quoted at BOTH its ends. The two skins stand on different arcs - the outer is
	// the longer, and by the crown it has run some forty inches ahead - so measuring each corner on its own skin
	// slides the outer corner along the band and the tile lines come out diagonal, by a different amount in every
	// cell. A flat annulus cannot unroll into a rectangle at all, so it is mapped like any other curved band: along
	// the ring, square to it, with the outer edge taking the stretch.
	static void Annulus(
		ArchMesh canvas,
		IReadOnlyList<Vector3> inner,
		IReadOnlyList<Vector3> outer,
		IReadOnlyList<float> developed,
		int column,
		ArchBrush brush,
		bool end )
	{
		// Each RADIAL EDGE its own thickness. The two rings part company at the invert - the inner springs off the
		// road, the outer carries on down past it - so the cell where the profile leaves the foot is a wedge, and
		// quoting the foot's depth to its far edge as well steps that edge against the arc facet standing on it.
		var here = (outer[column] - inner[column]).Length;
		var there = (outer[column + 1] - inner[column + 1]).Length;

		if ( end )
		{
			canvas.Ribbon( outer[column], inner[column], inner[column + 1], outer[column + 1], default, brush, new ArchWeave
			{
				A = new Vector2( here, developed[column] ),
				B = new Vector2( 0f, developed[column] ),
				C = new Vector2( 0f, developed[column + 1] ),
				D = new Vector2( there, developed[column + 1] )
			} );

			return;
		}

		canvas.Ribbon( inner[column], outer[column], outer[column + 1], inner[column + 1], default, brush, new ArchWeave
		{
			A = new Vector2( 0f, developed[column] ),
			B = new Vector2( here, developed[column] ),
			C = new Vector2( there, developed[column + 1] ),
			D = new Vector2( 0f, developed[column + 1] )
		} );
	}

	// A surface with no thickness laid a hair over another one - a marking, a crossing apron. Nothing to close where
	// a cut takes a cell: a skin has no body, so the hole is the missing cell itself.
	public static void Skin(
		ArchMesh canvas,
		IReadOnlyList<Vector3[]> rows,
		IReadOnlyList<ArchFrame> frames,
		ArchBrush brush,
		bool closed,
		IReadOnlyList<ArchCarveVolume> carves = null )
	{
		if ( rows.Count < 2 || rows[0].Length < 2 )
		{
			return;
		}

		var ribbed = Ribbed( rows, carves );
		var grid = ribbed.Rows;
		var across = ribbed.Across;
		var columns = ribbed.Columns;
		var segments = closed ? grid.Count : grid.Count - 1;
		var carved = Carved( grid, carves, closed );

		for ( var row = 0; row < segments; row++ )
		{
			var next = (row + 1) % grid.Count;
			var station = frames[row].Distance;
			var ahead = next == 0 ? station + (grid[next][0] - grid[row][0]).Length : frames[next].Distance;

			for ( var column = 0; column < columns; column++ )
			{
				if ( carved.Contains( (row, column) ) )
				{
					continue;
				}

				canvas.Ribbon( grid[row][column], grid[row][column + 1], grid[next][column + 1], grid[next][column],
					frames[row].Along, brush, Cell( across, row, next, column, station, ahead ) );
			}
		}
	}
}