Editor/Wall/ArchWallOpenings.cs

Editor code that computes and emits geometry for wall openings (windows, doors, grilles, casings, sills, thresholds). It builds meshes and boxes/quads from opening definitions, computes casing boards, grille frames and members, and door leaf requests for the wall generator.

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

namespace Sunless.Architecture;

// One resolution of what stands in a furnished hole - which grid, how coarse, which way it runs and how far
// proud - so the generator, a report and a test read the same answer rather than three copies of the kit
// arithmetic. Coordinates are the wall's own: x along, y across, z up.
public readonly struct ArchOpeningGrille
{
	public OpeningFurniture Furniture { get; init; }
	public ArchSurface Surface { get; init; }
	// Bars and a gate run UP the hole; a board and a shutter slat run across it.
	public bool Upright { get; init; }
	public float Border { get; init; }
	public float Member { get; init; }
	public float Gap { get; init; }
	// The y of the face it presents, and how deep it is behind that face.
	public float Face { get; init; }
	public float Depth { get; init; }
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }

	public Vector2 InnerMin => new( Min.x + Border, Min.y + Border );

	public Vector2 InnerMax => new( Max.x - Border, Max.y - Border );

	public float Pitch => Member + Gap;

	public bool Stands => Furniture != OpeningFurniture.None
		&& InnerMax.x - InnerMin.x > Member
		&& InnerMax.y - InnerMin.y > Member;

	// The count comes off ArchDivide over the hole, which is why no opening carries a bar count.
	public int Members => ArchDivide.AtMost( Upright ? InnerMax.x - InnerMin.x : InnerMax.y - InnerMin.y, Pitch ).Count;
}

// What a hole in a wall emits - the reveal lining it, the architrave round it, the sill under it, the fitting
// standing in it, the saddle across it and the leaf hung on it. The wall generator opens the hole and asks for
// the joinery; nothing here reads the wall back.
public static class ArchWallOpenings
{
	public static ArchSurface BooleanRevealSurface => ArchSurface.Trim;

	public static List<List<Vector2>> BooleanRevealLoops( IEnumerable<ArchOpening> openings, float wallHeight )
	{
		return ArchFootprint.Union( openings
			.Select( opening =>
			{
				var bottom = Math.Clamp( opening.SillHeight, 0f, wallHeight );
				var top = Math.Clamp( opening.Top, 0f, wallHeight );

				return (IReadOnlyList<Vector2>)ArchFootprint.Rect(
					new Vector2( opening.Left, bottom ),
					new Vector2( opening.Right, top ) );
			} )
			.Where( loop => loop[2].x - loop[0].x > 0.01f && loop[2].y - loop[0].y > 0.01f ) ).ToList();
	}

	public static void BooleanReveals( ArchMesh canvas, IEnumerable<ArchOpening> openings, float half, float wallHeight, ArchBrush brush )
	{
		foreach ( var loop in BooleanRevealLoops( openings, wallHeight ) )
		{
			for ( var index = 0; index < loop.Count; index++ )
			{
				var from = loop[index];
				var to = loop[(index + 1) % loop.Count];
				var horizontal = MathF.Abs( from.y - to.y ) < 0.01f;

				if ( horizontal && (MathF.Abs( from.y ) < 0.01f || MathF.Abs( from.y - wallHeight ) < 0.01f) )
				{
					continue;
				}

				canvas.Quad(
					new Vector3( from.x, half, from.y ),
					new Vector3( from.x, -half, from.y ),
					new Vector3( to.x, -half, to.y ),
					new Vector3( to.x, half, to.y ),
					brush );
			}
		}
	}

	// The architrave round the MERGED hole, off the same loops the reveals line. Two cuts biting one wall
	// overlap, and a frame per opening stands a jamb across the middle of the opening beside it.
	public static List<(Vector2 Min, Vector2 Max)> BooleanCasingBoards( IEnumerable<ArchOpening> openings, ArchKit kit, float wallHeight )
	{
		var boards = new List<(Vector2 Min, Vector2 Max)>();
		var cased = openings.Where( opening => opening.Cased ).ToList();
		var authored = cased.Select( opening => opening.CasingWidth ).FirstOrDefault( value => value > 0.1f );
		var width = authored > 0.1f ? authored : kit.CasingWidth;

		if ( cased.Count == 0 || width <= 0.1f || kit.CasingDepth <= 0.05f )
		{
			return boards;
		}

		var loops = BooleanRevealLoops( cased, wallHeight );
		var region = loops.Select( loop => (IReadOnlyList<Vector2>)loop ).ToList();
		var bite = ArchContact.Bite( kit );

		foreach ( var loop in loops )
		{
			for ( var index = 0; index < loop.Count; index++ )
			{
				if ( Architrave( region, loop[index], loop[(index + 1) % loop.Count], wallHeight, width, bite ) is { } board )
				{
					boards.Add( board );
				}
			}
		}

		return boards;
	}

	public static void BooleanCasing( ArchMesh canvas, IReadOnlyList<ArchOpening> openings, ArchKit kit, float half, float wallHeight, ArchBrush brush )
	{
		var boards = BooleanCasingBoards( openings, kit, wallHeight );

		foreach ( var face in new[] { half, -half } )
		{
			var sign = Math.Sign( face );
			var back = ArchContact.Bury( kit, face, sign );
			var near = MathF.Min( back, face + sign * kit.CasingDepth );
			var far = MathF.Max( back, face + sign * kit.CasingDepth );

			foreach ( var board in boards )
			{
				canvas.Box(
					new Vector3( board.Min.x, near, board.Min.y ),
					new Vector3( board.Max.x, far, board.Max.y ),
					brush );
			}
		}
	}

	// One board on one edge of the hole, lying outside it and lapping a bite over the arris. The head and
	// the apron run past the jambs so the frame closes at its corners; an edge that dies into the floor or
	// the wall head carries nothing, exactly as the reveal beside it does.
	static (Vector2 Min, Vector2 Max)? Architrave(
		IReadOnlyList<IReadOnlyList<Vector2>> region,
		Vector2 from,
		Vector2 to,
		float wallHeight,
		float width,
		float bite )
	{
		var span = to - from;
		var middle = (from + to) * 0.5f;

		if ( span.Length < 0.01f )
		{
			return null;
		}

		var outward = new Vector2( span.y, -span.x ).Normal;

		if ( ArchFootprint.Encloses( region, middle + outward * 0.1f ) )
		{
			outward = -outward;
		}

		var low = new Vector2( MathF.Min( from.x, to.x ), MathF.Min( from.y, to.y ) );
		var high = new Vector2( MathF.Max( from.x, to.x ), MathF.Max( from.y, to.y ) );

		if ( MathF.Abs( span.x ) < 0.01f )
		{
			var jamb = high.x + outward.x * width;
			// A jamb reaching the floor line runs ON DOWN into the slab, because the well trim round the hole
			// stands proud of it - held at the floor line the board ended in the air over its own nosing.
			var foot = low.y < 0.01f ? -width : low.y - width;

			return (new Vector2( MathF.Min( jamb, high.x - outward.x * bite ), foot ),
				new Vector2( MathF.Max( jamb, high.x - outward.x * bite ), MathF.Min( wallHeight, high.y + width ) ));
		}

		if ( MathF.Abs( high.y ) < 0.01f || MathF.Abs( high.y - wallHeight ) < 0.01f )
		{
			return null;
		}

		var rail = high.y + outward.y * width;

		return (new Vector2( low.x - width, MathF.Min( rail, high.y - outward.y * bite ) ),
			new Vector2( high.x + width, MathF.Max( rail, high.y - outward.y * bite ) ));
	}

	public static void Reveals( ArchMesh canvas, ArchOpening opening, float half, float wallHeight, bool sill, ArchBrush brush )
	{
		var left = opening.Left;
		var right = opening.Right;
		var bottom = Math.Max( 0f, opening.SillHeight );
		var top = Math.Min( wallHeight, opening.Top );

		if ( top <= bottom )
		{
			return;
		}

		canvas.Quad(
			new Vector3( left, -half, bottom ),
			new Vector3( left, half, bottom ),
			new Vector3( left, half, top ),
			new Vector3( left, -half, top ),
			brush );

		canvas.Quad(
			new Vector3( right, half, bottom ),
			new Vector3( right, -half, bottom ),
			new Vector3( right, -half, top ),
			new Vector3( right, half, top ),
			brush );

		canvas.Quad(
			new Vector3( left, half, top ),
			new Vector3( right, half, top ),
			new Vector3( right, -half, top ),
			new Vector3( left, -half, top ),
			brush );

		// A sill's top face owns this plane; lining the underside too would z-fight.
		if ( bottom > 0.01f && !sill )
		{
			canvas.Quad(
				new Vector3( left, -half, bottom ),
				new Vector3( right, -half, bottom ),
				new Vector3( right, half, bottom ),
				new Vector3( left, half, bottom ),
				brush );
		}
	}

	public static void Casing( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchBrush brush )
	{
		var width = opening.CasingWidth > 0f ? opening.CasingWidth : kit.CasingWidth;
		var depth = kit.CasingDepth;

		if ( width <= 0.1f || depth <= 0.05f )
		{
			return;
		}

		var left = opening.Left;
		var right = opening.Right;
		var bottom = Math.Max( 0f, opening.SillHeight );
		var top = Math.Min( wallHeight, opening.Top );
		var foot = Math.Max( 0f, bottom - width );

		// The head is the heavy member: glazed heads run deeper and past the jambs.
		var headWidth = opening.IsGlazed ? width * 1.7f : width;
		var headDepth = opening.IsGlazed ? depth * 1.7f : depth;
		var headRun = opening.IsGlazed ? width * 1.6f : width;

		// Bitten into what it lands on: exact coplanar faces are most opening z-fighting.
		var bite = ArchContact.Bite( kit );

		foreach ( var face in new[] { half, -half } )
		{
			var sign = Math.Sign( face );
			var back = ArchContact.Bury( kit, face, sign );
			var near = Math.Min( back, face + sign * depth );
			var far = Math.Max( back, face + sign * depth );
			var headNear = Math.Min( back, face + sign * headDepth );
			var headFar = Math.Max( back, face + sign * headDepth );

			canvas.Box( new Vector3( left - width, near, foot ), new Vector3( left + bite, far, top ), brush );
			canvas.Box( new Vector3( right - bite, near, foot ), new Vector3( right + width, far, top ), brush );
			canvas.Box( new Vector3( left - headRun, headNear, top - bite ), new Vector3( right + headRun, headFar, top + headWidth ), brush );

			if ( bottom > 0.01f )
			{
				canvas.Box( new Vector3( left, near, foot ), new Vector3( right, far, bottom + bite ), brush );
			}
		}
	}

	public static void Sill( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, ArchBrush brush )
	{
		var depth = kit.SillDepth;
		var thickness = kit.SillThickness;
		var overshoot = kit.CasingWidth;
		var outward = opening.IsGlazed ? depth * 2.2f : depth;

		canvas.Box(
			new Vector3( opening.Left - overshoot, -half - outward, opening.SillHeight - thickness ),
			new Vector3( opening.Right + overshoot, half + depth, opening.SillHeight ),
			brush );

		if ( !opening.IsGlazed )
		{
			return;
		}

		// The drip stops rain tracking back along the underside.
		canvas.Box(
			new Vector3( opening.Left - overshoot, -half - outward, opening.SillHeight - thickness - 1f ),
			new Vector3( opening.Right + overshoot, -half - outward + 1.3f, opening.SillHeight - thickness ),
			brush );
	}

	// What the kind may carry at all is ArchOpeningKinds' answer, reached through ArchOpening.Fitted; everything
	// else here is kit stock, which is why no hole carries a bar pitch, a proud or a frame width of its own.
	public static ArchOpeningGrille Grille( ArchOpening opening, ArchKit kit, float half, float wallHeight )
	{
		var fitted = opening.Fitted;
		var boarded = fitted == OpeningFurniture.Boarded;
		var bite = ArchContact.Bite( kit );
		var proud = MathF.Max( bite, kit.GrilleProud );
		var bottom = MathF.Max( 0f, opening.SillHeight );
		var top = MathF.Min( wallHeight, opening.Top );
		var span = ArchContact.Lapped( kit, opening.Left, opening.Right );
		var rise = ArchContact.Lapped( kit, bottom, top );

		return new ArchOpeningGrille
		{
			Furniture = fitted,
			// A shutter is a curtain of slats rather than a grille, and the palette is told so.
			Surface = fitted == OpeningFurniture.Shutter ? ArchSurface.Shutter : ArchSurface.Grille,
			Upright = fitted is OpeningFurniture.Bars or OpeningFurniture.Gate,
			// Boards are nailed straight across the hole; anything fitted inside one is welded into a frame.
			Border = boarded ? 0f : MathF.Max( 0f, kit.GrilleFrame ),
			Member = MathF.Max( 1f, boarded ? kit.FloorBoardWidth : kit.GrilleBar ),
			Gap = MathF.Max( 0f, boarded ? kit.FloorBoardGap : kit.GrilleGap ),
			// A gate hangs IN the frame it closes; a bar, a board and a shutter are fixed over the face of it.
			Face = fitted == OpeningFurniture.Gate ? -half + bite : -half - proud,
			Depth = proud + bite,
			// Boarding laps past the jambs onto the wall it is nailed to; a framed fitting keeps a bite clear of
			// the architrave that laps the other way into the hole.
			Min = boarded ? new Vector2( span.From, rise.From ) : new Vector2( opening.Left + bite, bottom + bite ),
			Max = boarded ? new Vector2( span.To, rise.To ) : new Vector2( opening.Right - bite, top - bite )
		};
	}

	public static void Furniture( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchStyle style, ArchPalette[] chain )
	{
		var grille = Grille( opening, kit, half, wallHeight );

		if ( !grille.Stands )
		{
			return;
		}

		var brush = style.Brush( grille.Surface, chain );

		using ( canvas.Part( ArchPieces.Bars ) )
		{
			GrilleFrame( canvas, grille, kit, brush );
			GrilleMembers( canvas, grille, brush );
		}
	}

	// Head and apron run INTO the jambs by a bite, the way the architrave outside them does: members that merely
	// butted would leave a coplanar pair at each corner of the frame.
	static void GrilleFrame( ArchMesh canvas, ArchOpeningGrille grille, ArchKit kit, ArchBrush brush )
	{
		if ( grille.Border < 0.05f )
		{
			return;
		}

		var bite = ArchContact.Bite( kit );
		var inner = grille.InnerMin;
		var outer = grille.InnerMax;
		var near = grille.Face;
		var far = grille.Face + grille.Depth;

		canvas.Box( new Vector3( grille.Min.x, near, grille.Min.y ), new Vector3( inner.x, far, grille.Max.y ), brush );
		canvas.Box( new Vector3( outer.x, near, grille.Min.y ), new Vector3( grille.Max.x, far, grille.Max.y ), brush );
		canvas.Box( new Vector3( inner.x - bite, near, grille.Min.y ), new Vector3( outer.x + bite, far, inner.y ), brush );
		canvas.Box( new Vector3( inner.x - bite, near, outer.y ), new Vector3( outer.x + bite, far, grille.Max.y ), brush );
	}

	// Through ArchPlanks - the one board emitter - so a burglar bar and a floor board are the same solid, and the
	// count comes off ArchDivide over the hole rather than a number typed against it.
	static void GrilleMembers( ArchMesh canvas, ArchOpeningGrille grille, ArchBrush brush )
	{
		var inner = grille.InnerMin;
		var outer = grille.InnerMax;
		var region = new[] { (IReadOnlyList<Vector2>)ArchFootprint.Rect( inner, outer ) };

		var spec = new ArchPlankSpec
		{
			Width = grille.Member,
			Gap = grille.Gap,
			Thickness = grille.Depth,
			// Four times the diagonal, so no stagger joint can land inside the hole: a bar is one length of steel
			// and a shutter slat runs the whole width of the curtain.
			Length = (outer - inner).Length * 4f,
			Top = grille.Face + grille.Depth,
			Yaw = grille.Upright ? 90f : 0f,
			Soffit = true
		};

		ArchPlanks.Fill( canvas, region, null, spec, brush, ArchPlankPlane.Upright );
	}

	// Slabs stop at the centreline; the saddle spans both faces, standing on the higher finish.
	public static float Threshold( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float floor, ArchBrush brush )
	{
		var proud = MathF.Max( 0f, kit.ThresholdProud );

		if ( !Saddled( opening ) || proud < 0.05f )
		{
			return 0f;
		}

		var lap = MathF.Max( 0f, kit.ThresholdLap );
		var top = floor + proud;
		var span = ArchContact.Lapped( kit, opening.Left, opening.Right );

		canvas.Box(
			new Vector3( span.From, -half - lap, ArchContact.Bury( kit, 0f, 1f ) ),
			new Vector3( span.To, half + lap, top ),
			brush );

		return top;
	}

	// Uncased holes are breaches; a garage's driveway runs over it, so neither saddles.
	static bool Saddled( ArchOpening opening )
	{
		return opening.Cased && opening.SillHeight < 0.5f && opening.Kind != OpeningKind.Garage;
	}

	public static void Leaves( List<ArchDoorRequest> doors, ArchOpening opening, ArchWall wall, ArchKit kit, float saddle, ArchBrush brush )
	{
		if ( doors is null )
		{
			return;
		}

		// The leaf clears the saddle; standing on it would sweep the face as it swings.
		var clearance = saddle + MathF.Max( 0f, kit.DoorUndercut );
		var foot = opening.SillHeight + clearance;
		var height = opening.Height - clearance;
		var thickness = kit.DoorLeafThickness;

		if ( height < 1f )
		{
			return;
		}

		if ( opening.Kind == OpeningKind.DoubleDoor )
		{
			var leaf = opening.Width * 0.5f;

			doors.Add( Request( opening, wall, new Vector3( opening.Left, 0f, foot ), leaf, height, thickness, false, brush ) );
			doors.Add( Request( opening, wall, new Vector3( opening.Right, 0f, foot ), leaf, height, thickness, true, brush ) );
			return;
		}

		var hingeX = opening.FlipHinge ? opening.Right : opening.Left;
		doors.Add( Request( opening, wall, new Vector3( hingeX, 0f, foot ), opening.Width, height, thickness, opening.FlipHinge, brush ) );
	}

	static ArchDoorRequest Request( ArchOpening opening, ArchWall wall, Vector3 hinge, float width, float height, float thickness, bool mirrored, ArchBrush brush )
	{
		return new ArchDoorRequest
		{
			Opening = opening,
			Wall = wall,
			HingeLocal = hinge,
			LeafWidth = width,
			LeafHeight = height,
			LeafThickness = thickness,
			Mirrored = mirrored,
			Brush = brush
		};
	}
}