Editor/Wall/ArchWallGen.cs

Editor-side generator for wall geometry and fittings. It builds wall mesh panels, trims, openings, windows, doors, baseboards and related decorative bands using room/building/style/kit data and writes quads to an ArchMesh canvas.

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

namespace Sunless.Architecture;

public static partial class ArchWallGen
{
	public static void Build(
		ArchMesh canvas,
		ArchWall wall,
		ArchRoom room,
		ArchBuilding building,
		ArchKit kit,
		ArchStyle style,
		List<ArchDoorRequest> doors,
		List<ArchGlassRequest> glazing,
		ArchConnectionService connections = null,
		ArchPlan plan = null )
	{
		if ( connections is null )
		{
			connections = new ArchConnectionService( building, kit );
		}

		var floorAbove = ArchStoreyGeometry.FloorAbove( room, building, kit, wall.Start, wall.End );
		var storey = floorAbove > room.BaseHeight + 0.5f ? floorAbove - room.BaseHeight : 0f;

		var height = ArchWallSection.Height( wall, room, kit );
		var thickness = ArchWallSection.Thickness( wall, kit );
		var length = wall.Length;

		// A wall authored short of the ceiling is a half wall: it keeps the head it was given and is
		// finished by its own cap, where a full-height one is finished by whatever stands over it.
		var freeHead = wall.Height > 0.5f && wall.Height < MathF.Max( storey, ArchWallSection.CeilingHeight( room, kit ) ) - 0.5f;

		// A wall under the storey above runs to that floor, not its own height.
		var roofed = storey > 0f && !freeHead;

		// A roof over the wall finishes it; capping anyway would sit under the deck. A link's deck and
		// slab both stop on the wall CENTRELINE, so a spanning wall is never finished by them.
		var covered = !freeHead && (roofed || Roofed( wall, room, building )) && !room.Spans;

		if ( roofed )
		{
			height = MathF.Max( height, storey );
		}

		if ( length < 1f || height < 1f )
		{
			return;
		}

		var half = thickness * 0.5f;
		var join = connections.Wall( wall, room )
			.WithThickness( thickness )
			.Create();
		var chain = new[] { wall.Palette, room.Palette, building.Palette };

		// Both faces always emitted; skin derives from Enclosed - every wall is authored Exterior.
		var enclosed = connections.Enclosed( wall, room, half );

		var cladding = wall.Cladding != WallCladding.None
			? ArchSurface.Siding
			: wall.Exterior && !enclosed ? ArchSurface.WallExterior : ArchSurface.WallInterior;

		var exterior = style.Brush( cladding, chain );
		var interior = style.Brush( ArchSurface.WallInterior, chain );
		var reveal = style.Brush( ArchSurface.Reveal, chain );
		var trim = style.Brush( ArchSurface.Trim, chain );

		// A breached archway belongs to the flight that walked through here, so it closes with it.
		// A unit a cut swallowed drops out whole - frame, sashes, casing and sill. The hole it stood in is
		// the cut's own archway, which covers it, so the wall stays open where the window was.
		var openings = wall.Openings
			.Where( opening => opening.Width > 0.5f && opening.Height > 0.5f )
			.Where( opening => ArchLayerGate.On( opening ) && ArchLayerGate.Owned( opening.OwnerId ) )
			.Where( opening => opening.SwallowedBy == 0 || !ArchLayerGate.Owned( opening.SwallowedBy ) )
			.OrderBy( opening => opening.Left )
			.ToList();

		var cuts = openings
			.Concat( connections.SharedOpenings( wall, room, thickness ) )
			.OrderBy( opening => opening.Left )
			.ToList();
		var cutIds = plan?.AllCuts().Select( cut => cut.Id ).ToHashSet()
			?? new HashSet<int>();
		var booleanOpenings = openings.Where( opening => cutIds.Contains( opening.OwnerId ) ).ToList();

		var from = join.From;
		var to = join.To;
		var interiorFrom = join.SkirtFrom;
		var interiorTo = join.SkirtTo;
		var exteriorFrom = join.StartExtend;
		var exteriorTo = join.EndExtend;

		// The finished floor, not the slab; opening cuts deliberately keep the structural datum.
		var finish = Finish( room, kit );

		var modifiers = ArchWallModShape.For( wall, join, height, thickness, kit );

		modifiers.AddRange( ArchWallDamage.CornerRecesses( plan, kit, building, room, wall, exteriorFrom, exteriorTo, height, thickness ) );

		var bitten = modifiers.Where( shape => shape.Carves ).ToList();
		var carvedOutside = bitten.Where( shape => shape.Outside ).ToList();
		var carvedInside = bitten.Where( shape => shape.Inside ).ToList();
		var proudOutside = modifiers.Where( shape => !shape.Carves && shape.Outside ).ToList();
		var proudInside = modifiers.Where( shape => !shape.Carves && shape.Inside ).ToList();

		foreach ( var panel in Panels( from, to, height, cuts, join.Landings ) )
		{
			var innerFrom = MathF.Max( panel.MinX, interiorFrom );
			var innerTo = MathF.Min( panel.MaxX, interiorTo );

			if ( !wall.SingleSided && innerTo - innerFrom > 0.01f )
			{
				foreach ( var patch in Faced( panel with { MinX = innerFrom, MaxX = innerTo }, carvedInside ) )
				{
					canvas.Quad(
						new Vector3( patch.MinX, half, patch.MinZ ),
						new Vector3( patch.MinX, half, patch.MaxZ ),
						new Vector3( patch.MaxX, half, patch.MaxZ ),
						new Vector3( patch.MaxX, half, patch.MinZ ),
						interior );
				}
			}

			var outerFrom = MathF.Max( panel.MinX, exteriorFrom );
			var outerTo = MathF.Min( panel.MaxX, exteriorTo );

			if ( outerTo - outerFrom <= 0.01f )
			{
				continue;
			}

			foreach ( var course in Banded( panel, wall.Bands, height ) )
			{
				var field = course.Panel;
				var xFrom = MathF.Max( field.MinX, outerFrom );
				var xTo = MathF.Min( field.MaxX, outerTo );

				if ( xTo - xFrom <= 0.01f )
				{
					continue;
				}

				var skin = course.Field is { } role ? style.Brush( role, chain ) : exterior;

				foreach ( var patch in Faced( field with { MinX = xFrom, MaxX = xTo }, carvedOutside ) )
				{
					canvas.Quad(
						new Vector3( patch.MinX, -half, patch.MinZ ),
						new Vector3( patch.MaxX, -half, patch.MinZ ),
						new Vector3( patch.MaxX, -half, patch.MaxZ ),
						new Vector3( patch.MinX, -half, patch.MaxZ ),
						skin );

					if ( course.Field is null )
					{
						Clad( canvas, patch, half, kit, wall.Cladding, exterior );
					}
				}
			}
		}

		using ( canvas.Part( ArchPieces.Trims ) )
		{
			BandCourses( canvas, wall, room, building, plan, kit, join, height, half, proudOutside, style.Brush( ArchSurface.Trim, chain ) );
			Mouldings( canvas, wall, room, building, plan, kit, join, height, half, proudOutside, proudInside, style, chain );
			Pilasters( canvas, wall, room, building, plan, kit, join, height, half, style.Brush( ArchSurface.Pillar, chain ) );
		}

		using ( canvas.Part( ArchPieces.Fittings ) )
		{
			Modifiers( canvas, modifiers, half, style, chain );
			ArchDamage.WallCorners( canvas, plan, kit, building, room, wall, exteriorFrom, exteriorTo, height, thickness, style, connections );
		}

		// The caps close the wall's own box, so they stay on its body - split off, the shell they seal is a
		// solid with three open sides and nothing says which part it belongs to.
		CapEnds( canvas, height, half, join, reveal, exterior );
		CapTop( canvas, wall, room, building, plan, kit, join, covered, height, half, style, chain );

		if ( room.Spans )
		{
			CapFoot( canvas, join, half, style, chain );
		}

		using ( canvas.Part( ArchPieces.Openings ) )
		{
			ArchWallOpenings.BooleanReveals( canvas, booleanOpenings, half, height, style.Brush( ArchWallOpenings.BooleanRevealSurface, chain ) );
			ArchWallOpenings.BooleanCasing( canvas, booleanOpenings, kit, half, height, trim );

			foreach ( var opening in openings.Where( opening => !cutIds.Contains( opening.OwnerId ) ) )
			{
				// Opening leads the palette chain, so one window can vary without touching the building.
				var glazed = opening.IsGlazed;
				var glass = new[] { opening.Palette, wall.Palette, room.Palette, building.Palette };
				var sill = NeedsSill( opening );

				// Lining and architrave follow the unit in the hole; archways have none.
				var joinery = opening.Kind switch
				{
					OpeningKind.Window => style.Brush( ArchSurface.WindowFrame, glass ),
					OpeningKind.Door or OpeningKind.DoubleDoor or OpeningKind.Garage => style.Brush( ArchSurface.DoorLeaf, chain ),
					_ => (ArchBrush?)null
				};

				// An archway holds nothing to line: its jambs are the wall's own finish carried through the
				// thickness, and a lining skin there bands the passage in another material at the mouth.
				var lining = opening.Kind == OpeningKind.Archway ? interior : reveal;

				ArchWallOpenings.Reveals( canvas, opening, half, height, sill, joinery ?? lining );

				if ( opening.Cased )
				{
					ArchWallOpenings.Casing( canvas, opening, kit, half, height, joinery ?? trim );
				}

				if ( sill )
				{
					ArchWallOpenings.Sill( canvas, opening, kit, half, style.Brush( ArchSurface.Sill, chain ) );
				}

				ArchWallOpenings.Furniture( canvas, opening, kit, half, height, style, glass );

				var saddle = ArchWallOpenings.Threshold( canvas, opening, kit, half,
					MathF.Max( finish, Finish( connections.RoomAcross( wall, room, opening.Offset, half ), kit ) ),
					style.Brush( ArchSurface.Threshold, chain ) );

				if ( glazed )
				{
					var panes = opening.Breakable ? canvas.Sibling() : canvas;

					ArchWindowGen.Build(
						canvas, panes, opening, kit, half, height,
						style.Brush( ArchSurface.WindowFrame, glass ),
						style.Brush( ArchSurface.WindowSash, glass ),
						style.Brush( ArchSurface.Glass, glass ) );

					if ( opening.Breakable )
					{
						glazing.Add( new ArchGlassRequest { Opening = opening, Canvas = panes } );
					}
				}

				if ( opening.IsHung )
				{
					ArchWallOpenings.Leaves( doors, opening, wall, kit, saddle, style.Brush( ArchSurface.DoorLeaf, chain ) );
				}
			}
		}

		using ( canvas.Part( ArchPieces.Trims ) )
		{
			if ( wall.Baseboard )
			{
				using ( canvas.Part( ArchPieces.Skirting ) )
				{
					Baseboards( canvas, wall, room, building, plan, join, half, kit, cuts, style.Brush( ArchSurface.Baseboard, chain ),
						Skirting( wall, room, building, plan, kit, half, height ), finish );
				}
			}

			if ( wall.Wainscot )
			{
				Wainscot( canvas, join.SkirtFrom, join.SkirtTo, half, kit, cuts, style.Brush( ArchSurface.Wainscot, chain ), trim, finish );
			}
		}
	}

	static bool Roofed( ArchWall wall, ArchRoom room, ArchBuilding building )
	{
		return ArchOpeningClearance.Covering( building, room, wall ) is not null;
	}

	static float Finish( ArchRoom room, ArchKit kit )
	{
		if ( room is null || !room.FloorBoards )
		{
			return 0f;
		}

		return MathF.Max( 0f, kit.FloorBoardThickness );
	}

	static bool NeedsSill( ArchOpening opening )
	{
		return opening.IsSilled && opening.SillHeight > 0.5f;
	}

	public readonly struct Panel
	{
		public float MinX { get; init; }
		public float MaxX { get; init; }
		public float MinZ { get; init; }
		public float MaxZ { get; init; }
	}

	// Field is the role; null = the wall's own cladding above the top band.
	public readonly struct Course
	{
		public Panel Panel { get; init; }
		public ArchSurface? Field { get; init; }
	}

	// Bands cut the same z split as openings, so fields share one reveal set.
	public static IEnumerable<Course> Banded( Panel panel, IEnumerable<ArchWallBand> bands, float wallHeight )
	{
		var cursor = panel.MinZ;

		foreach ( var band in ArchWallTreatments.Ordered( bands, wallHeight ) )
		{
			var at = band.At( wallHeight );

			// Neither skip is an error: a door-head panel sits above the brick line.
			if ( at <= cursor + 0.05f )
			{
				continue;
			}

			if ( at >= panel.MaxZ - 0.05f )
			{
				break;
			}

			yield return new Course
			{
				Panel = panel with { MinZ = cursor, MaxZ = at },
				Field = band.Field
			};

			cursor = at;
		}

		if ( panel.MaxZ - cursor > 0.01f )
		{
			yield return new Course { Panel = panel with { MinZ = cursor, MaxZ = panel.MaxZ }, Field = null };
		}
	}

	public static IEnumerable<Panel> Panels( float from, float to, float height, IReadOnlyList<ArchOpening> openings,
		IReadOnlyList<float> splits = null )
	{
		var cuts = new List<float> { from, to };

		foreach ( var opening in openings )
		{
			cuts.Add( Math.Clamp( opening.Left, from, to ) );
			cuts.Add( Math.Clamp( opening.Right, from, to ) );
		}

		// A junction splits the field like an opening, so the landed-on quad ends exactly there.
		foreach ( var split in splits ?? Array.Empty<float>() )
		{
			if ( split > from + 0.01f && split < to - 0.01f )
			{
				cuts.Add( split );
			}
		}

		cuts = cuts.Distinct().OrderBy( value => value ).ToList();

		for ( var index = 0; index < cuts.Count - 1; index++ )
		{
			var minX = cuts[index];
			var maxX = cuts[index + 1];

			if ( maxX - minX < 0.01f )
			{
				continue;
			}

			var centre = (minX + maxX) * 0.5f;

			var holes = openings
				.Where( opening => centre > opening.Left && centre < opening.Right )
				.Select( opening => (Bottom: Math.Max( 0f, opening.SillHeight ), Top: Math.Min( height, opening.Top )) )
				.Where( hole => hole.Top > hole.Bottom )
				.OrderBy( hole => hole.Bottom )
				.ToList();

			var cursor = 0f;

			foreach ( var hole in holes )
			{
				if ( hole.Bottom - cursor > 0.01f )
				{
					yield return new Panel { MinX = minX, MaxX = maxX, MinZ = cursor, MaxZ = hole.Bottom };
				}

				cursor = Math.Max( cursor, hole.Top );
			}

			if ( height - cursor > 0.01f )
			{
				yield return new Panel { MinX = minX, MaxX = maxX, MinZ = cursor, MaxZ = height };
			}
		}
	}

	static IEnumerable<(float From, float To)> FittingRuns(
		ArchPlan plan,
		ArchKit kit,
		ArchRoom room,
		ArchBuilding building,
		ArchWall wall,
		float from,
		float to,
		float offset,
		float bottom,
		float top )
	{
		var start = wall.Start + wall.Direction * from + wall.Normal * offset;
		var finish = wall.Start + wall.Direction * to + wall.Normal * offset;

		foreach ( var span in ArchCut.Outside(
			plan,
			kit,
			room.Floor,
			building?.Id ?? 0,
			start,
			finish,
			room.BaseHeight + bottom,
			room.BaseHeight + top,
			ArchCutAffects.WallFittings,
			wall.Id ) )
		{
			yield return (from + (to - from) * span.From, from + (to - from) * span.To);
		}
	}

	static void Run(
		ArchMesh canvas,
		ArchBandEnd from,
		ArchBandEnd to,
		ArchWall wall,
		ArchRoom room,
		ArchBuilding building,
		ArchPlan plan,
		float half,
		float depth,
		float height,
		ArchKit kit,
		ArchBrush brush,
		float lift )
	{
		foreach ( var span in FittingRuns( plan, kit, room, building, wall, from.Inner, to.Inner, half + depth * 0.5f, lift, lift + height ) )
		{
			var start = EndAt( from, to, span.From, true );
			var finish = EndAt( from, to, span.To, false );

			ArchBandGen.Run( canvas, start, finish, ArchBandFrame.Local, half, depth, ArchContact.Bury( kit, lift, 1f ), lift + height, brush );
		}
	}

	static ArchBandEnd EndAt( ArchBandEnd from, ArchBandEnd to, float at, bool start )
	{
		var span = to.Inner - from.Inner;
		var amount = span < 0.001f ? 0f : (at - from.Inner) / span;
		var original = start ? from : to;
		var stop = MathF.Abs( amount - (start ? 0f : 1f) ) < 0.001f ? original.Stop : ArchBandStop.Cut;
		var returned = stop == ArchBandStop.Returned ? original.Return : 0f;

		return new ArchBandEnd( at, from.Outer + (to.Outer - from.Outer) * amount, stop, returned );
	}
}