Editor/Roof/ArchRoofGen.cs

Editor generator for roof geometry. It builds roof regions, decks, soffits, fascias, parapets, interior ceilings and related mesh parts into an ArchMesh using plan, kit and style data.

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

namespace Sunless.Architecture;

public static partial class ArchRoofGen
{
	public static void Build( ArchMesh canvas, ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, List<ArchRunPath> gutterRuns )
	{
		foreach ( var region in new ArchRoofRegionService( building, roof ).Resolve() )
		{
			BuildRegion( canvas, roof, building, plan, kit, style, gutterRuns, region );
		}
	}

	static void BuildRegion( ArchMesh canvas, ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, List<ArchRunPath> gutterRuns, ArchRoofRegion region )
	{
		var outline = region.Outer;
		var shapeStyle = roof.Style;
		var chain = new[] { roof.Palette, building.Palette };
		var deck = style.Brush( ArchSurface.Roof, chain );
		var edge = style.Brush( ArchSurface.RoofEdge, chain );
		var kinds = ArchKinds.Load();

		// A storey above stops the overhang ring at the wall line, so only a ceiling is left.
		if ( BuiltOver( roof, building, outline ) )
		{
			Interior( canvas, roof, region.Loops, building, plan, kit, style, chain, kinds );
			return;
		}

		var abutments = Abutments( roof, building, kinds );
		var junctions = ArchCrossGableService.Resolve( building, kit, kinds );
		var fittingAbutments = FittingAbutments( roof, building, plan, kit, junctions, kinds );
		var overhang = ArchRoofPlane.Overhang( roof );
		var thickness = ArchRoofPlane.Thickness( roof, kit );

		bool Abuts( Vector2 point, Vector2 outward ) => Buried( abutments, point + outward * ArchProbe.Step );
		bool FittingAbuts( Vector2 point, Vector2 outward ) => Buried( fittingAbutments, point + outward * ArchProbe.Step );

		// Raked gable ends and a shed's high side shed nothing, so they carry no fascia or soffit.
		bool Bare( Vector2 point, Vector2 outward ) => FittingAbuts( point, outward ) || !Sheds( roof, shapeStyle, outward );

		// Only a TALLER roof takes the slot away. A deck arriving level with the plate covers the closure
		// instead, so it laps into that - held clear of it the way a board is, the plate is left open along
		// the whole mouth and you look into the roof from inside the passage.
		bool Slotless( Vector2 point, Vector2 outward ) => Abuts( point, outward ) || !Sheds( roof, shapeStyle, outward );

		var drip = ArchRoofPlane.Drip( roof, kit );
		var rafters = ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, overhang + drip ), abutments );
		var eaves = ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, overhang ), fittingAbutments );
		// The soffit bands the plate line, so it is notched the way the eave loop is: left whole it only had
		// the whole-edge break to fall back on, and a link taking 192 in out of a 448 in eave took the boards
		// off all of it.
		var soffits = ArchFootprint.Subtract( region.Loops, fittingAbutments );
		var plates = ArchFootprint.Subtract( region.Loops, abutments );

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

		Bounds( rafters, out var deckMin, out var deckMax );

		switch ( shapeStyle )
		{
			case RoofStyle.Flat:
				using ( canvas.Part( ArchPieces.Deck ) )
				{
					Deck( canvas, roof, building, rafters, deckMin, deckMax, thickness, plan, kit, style, chain, deck );
				}

				if ( roof.RidgeCap )
				{
					using ( canvas.Part( ArchPieces.Capping ) )
					{
						Capping( canvas, Abutted( rafters, Abuts, roof.BaseHeight ), thickness, kit, edge, plan, roof.Level, building.Id );
					}
				}
				break;

			case RoofStyle.Hip:
				var shape = ArchRoofSkeleton.Shape( rafters, Abuts, roof.BaseHeight, ArchRoofPlane.Slope( roof ) );
				var faceted = LitFacets( roof );

				using ( canvas.Part( ArchPieces.Deck ) )
				{
					JoinedSlab( canvas, Pierced( shape.Faces, roof, building, kit, thickness, plan, faceted ), Vector3.Up * thickness, deck );
				}

				if ( roof.RidgeCap )
				{
					using ( canvas.Part( ArchPieces.Capping ) )
					{
						Capping( canvas, shape.Creases, thickness, kit, edge, plan, roof.Level, building.Id );
						Capping( canvas, shape.Flashing, thickness, kit, edge, plan, roof.Level, building.Id );
					}
				}

				foreach ( var piece in faceted )
				{
					using ( canvas.Part( piece.Light.Form.Piece() ) )
					{
						Light( canvas, piece, thickness, kit, style, chain );
					}
				}
				break;

			case RoofStyle.Shed:
			case RoofStyle.Gable:
				using ( canvas.Part( ArchPieces.Deck ) )
				{
					Deck( canvas, roof, building, rafters, deckMin, deckMax, thickness, plan, kit, style, chain, deck );
				}
				break;

			case RoofStyle.Sawtooth:
				using ( canvas.Part( ArchPieces.Deck ) )
				{
					Sawtooth( canvas, roof, rafters, deckMin, deckMax, thickness, deck, edge );
				}
				break;
		}

		if ( roof.Frame )
		{
			using ( canvas.Part( ArchPieces.Frame ) )
			{
				Frame( canvas, roof, rafters, kit, style.Brush( ArchSurface.RoofFrame, chain ) );
			}
		}

		if ( roof.EndWalls )
		{
			var walls = style.Brush( ArchSurface.WallExterior, chain );

			using ( canvas.Part( ArchPieces.EndWalls ) )
			{
				EndWalls( canvas, roof, deckMin, deckMax, region.Loops, kit, walls, abutments, junctions );
				Closure( canvas, roof, plates, kit, walls, overhang + drip, Slotless, plan, building.Id );
			}
		}

		// The eave assembly, one row: the boards, the channel hung under them and the pipes off its outlets are
		// the thing an author reaches for, and each was a separate hunt down the roof.
		if ( roof.Soffit && overhang > 0.5f )
		{
			using ( canvas.Part( ArchPieces.Gutters ) )
			using ( canvas.Part( ArchPieces.Soffit ) )
			{
				Soffit( canvas, roof, soffits, kit, style.Brush( ArchSurface.Soffit, chain ), Bare, plan, building.Id );
			}
		}

		if ( roof.Fascia )
		{
			using ( canvas.Part( ArchPieces.Gutters ) )
			using ( canvas.Part( ArchPieces.Fascia ) )
			{
				Fascia( canvas, roof, eaves, kit, edge, Bare, plan, building.Id );
			}
		}

		if ( roof.Parapet )
		{
			using ( canvas.Part( ArchPieces.Parapet ) )
			{
				Parapet( canvas, roof, outline, kit, style.Brush( ArchSurface.WallExterior, chain ), style.Brush( ArchSurface.WallCap, chain ), Abuts, plan, building.Id );
			}
		}

		if ( roof.Guardrail )
		{
			using ( canvas.Part( ArchPieces.Guardrail ) )
			{
				Guardrail( canvas, roof, outline, kit, style, chain, Abuts );
			}
		}

		Interior( canvas, roof, region.Loops, building, plan, kit, style, chain, kinds );

		if ( gutterRuns is not null )
		{
			foreach ( var run in GutterRuns( roof, building, kit, plan, region ) )
			{
				gutterRuns.AddRange( ArchCut.Runs( plan, kit, roof.Level, building.Id, run, ArchCutAffects.Gutters ) );
			}
		}
	}

	static void Interior( ArchMesh canvas, ArchRoofPart roof, List<List<Vector2>> region, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, ArchPalette[] chain, ArchKinds kinds )
	{
		if ( !roof.Ceiling && !roof.VaultedCeiling )
		{
			return;
		}

		if ( roof.VaultedCeiling )
		{
			using ( canvas.Part( ArchPieces.Ceiling ) )
			{
				ArchVaultGen.Build( canvas, roof, kit, style );
			}

			return;
		}

		// LAPPED INTO the deck, not held off it. Level with the soffit, the ceiling's top and the deck's
		// underside are one plane the eave line runs through - a coplanar pair on a non-manifold edge. Held
		// short of it instead, the slot shows at the mouth of a link. So the top buries in the deck and the
		// soffit stays exactly where it was, which is what the cornice under it is measured from.
		var head = roof.BaseHeight + ArchContact.Bite( kit );
		var ceiling = roof.BaseHeight - kit.FloorThickness;
		var surface = roof.LoftFloor ? ArchSurface.Floor : ArchSurface.Ceiling;
		var (bitten, recesses) = ArchFloorGen.Bites( plan, roof.Level + 1, kit, ceiling, head, building.Id, ArchCutAffects.Roofs, roof.Id );

		var wells = ArchCut.Stored( building, roof.Level + 1 )
			.Concat( bitten )
			.Where( cutout => Reaches( region, cutout ) )
			.ToList();

		// Handed over, not doubled: where a room carries its own ceiling this slab steps aside, so the two
		// never stand an inch apart with a coplanar pair down every wall between them.
		var handed = wells.Concat( HandedOver( roof, building ) ).ToList();

		using ( canvas.Part( ArchPieces.Ceiling ) )
		{
			ArchFloorGen.Solid( canvas, region, handed, ceiling, head, style.Brush( surface, chain ), recesses );
			ArchFloorGen.WellTrim( canvas, wells, ceiling, head, kit, style.Brush( ArchSurface.Trim, chain ),
				ArchWellEdges.Trimmed( building, kit, wells, head ),
				building.Rooms
					.SelectMany( room => plan.Filed( ArchKind.Wall, room, kinds ) )
					.OfType<ArchWall>()
					.ToList(), region );
		}

		using ( canvas.Part( ArchPieces.Ceiling ) )
		using ( canvas.Part( ArchPieces.Cornice ) )
		{
			foreach ( var outline in region )
			{
				if ( roof.Walkway )
				{
					WalkwayCornice( canvas, roof, wells, kit, ceiling, style.Brush( ArchSurface.Trim, chain ) );
					break;
				}

				ArchCornice.Around( canvas, outline, wells, kit, ceiling, style.Brush( ArchSurface.Trim, chain ) );
			}
		}
	}

	// A vault or a loft floor is the roof's own shape and keeps the whole region; anything else gives way
	// to the rooms that carry a ceiling themselves.
	static IEnumerable<ArchFloorCutout> HandedOver( ArchRoofPart roof, ArchBuilding building )
	{
		if ( ArchFloorGen.RoofKeepsCeiling( roof ) )
		{
			yield break;
		}

		foreach ( var room in building.Rooms.Where( room => room.Floor == roof.Level && room.HasCeiling ) )
		{
			var footprint = ArchFloorGen.Footprint( room );

			if ( footprint.Count < 3 )
			{
				continue;
			}

			var handed = new ArchFloorCutout { Level = roof.Level, Name = room.Name };

			handed.Reshape( footprint );

			yield return handed;
		}
	}

	// A ceiling trims the wells it is actually under. Handed every cutout the building owns, a small
	// deck in one corner - a walkway's - ran a band of trim round a stairwell hundreds of inches away.
	static bool Reaches( IReadOnlyList<List<Vector2>> region, ArchFloorCutout cutout )
	{
		foreach ( var loop in region )
		{
			ArchFootprint.Bounds( loop, out var min, out var max );

			if ( ArchFootprint.Overlaps(
					new ArchBox { Min = cutout.Min, Max = cutout.Max },
					new ArchBox { Min = min, Max = max } ) )
			{
				return true;
			}
		}

		return false;
	}

	// Only a FULL storey; partial overlaps are left to the abutment clip.
	static bool BuiltOver( ArchRoofPart roof, ArchBuilding building, IReadOnlyList<Vector2> outline )
	{
		return ArchRegion.Covers( ArchRegion.Storey( building, roof.Level + 1 ), outline );
	}

	// A higher eave buries what runs into it, so its footprint clips the overhang.
	static List<List<Vector2>> Abutments( ArchRoofPart roof, ArchBuilding building, ArchKinds kinds )
	{
		return ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds )
			.OfType<ArchRoofPart>()
			.Where( other => !ReferenceEquals( other, roof ) && other.BaseHeight > roof.BaseHeight + 1f )
			.Select( other => ArchFootprint.Wind( other.Outline() ) )
			.ToList();
	}

	// What a FITTING cannot hang in: a deck standing LEVEL with this eave is in the way even though the
	// eave itself carries on - a link's deck meets the house at its own eave height, and the gutter ran
	// straight over the passage and hung inside it under the ceiling. Only a HIGHER roof buries the eave,
	// which is a house's own roof over its own wall. Filed on one of the two houses it joins, a link
	// stands in the eave of both, so the neighbours count and the plan is how they are seen.
	static List<List<Vector2>> Blocking( ArchRoofPart roof, ArchBuilding building, ArchPlan plan, ArchKinds kinds )
	{
		// A ghosted roof stands on a building the plan does not hold, so its neighbours are that building's alone.
		var standing = plan is not null
			? plan.AllRoofs( kinds )
			: ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds ).OfType<ArchRoofPart>();

		return standing
			.Where( other => !ReferenceEquals( other, roof ) && other.BaseHeight > roof.BaseHeight - 1f )
			.Select( other => ArchFootprint.Wind( other.Outline() ) )
			.ToList();
	}

	// Everything hanging on this eave stops short of the same footprints - fascia, soffit, closure and
	// channel - or a board runs on past where the channel gave up. Padded off the wall each of these
	// stands on: an outline is a wall CENTRELINE, so a run to it dies half a thickness inside the wall
	// it meets instead of stopping clear of its face.
	static List<List<Vector2>> FittingAbutments(
		ArchRoofPart roof,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		IReadOnlyList<ArchCrossGableJunction> junctions,
		ArchKinds kinds )
	{
		return ArchFootprint.Grow(
			Blocking( roof, building, plan, kinds )
				.Concat( ArchCrossGableService.FittingAbutments( roof, kit, junctions ) )
				.ToList(),
			kit.WallThickness * 0.5f + ArchContact.Bite( kit ) );
	}

	static bool Buried( List<List<Vector2>> abutments, Vector2 point )
	{
		return abutments.Any( loop => ArchFloorGen.Contains( loop, point ) );
	}

	// The same split GutterRuns makes - a board and a channel must agree on the eave.
	static bool Sheds( ArchRoofPart roof, RoofStyle style, Vector2 outward )
	{
		if ( style == RoofStyle.Gable )
		{
			return MathF.Abs( roof.RidgeAlongX ? outward.y : outward.x ) > 0.5f;
		}

		if ( style != RoofStyle.Shed )
		{
			return true;
		}

		var fall = roof.Reversed ? 1f : -1f;

		return (roof.RidgeAlongX ? outward.y * fall : outward.x * fall) > 0.5f;
	}

	static bool Rectangular( ArchRoofRegion region )
	{
		return Rectangular( region.Loops );
	}

	static bool Rectangular( IReadOnlyList<List<Vector2>> loops )
	{
		return loops.Count == 1 && loops[0].Count == 4;
	}

	static void Bounds( List<List<Vector2>> loops, out Vector2 min, out Vector2 max )
	{
		ArchFootprint.Bounds( loops.SelectMany( loop => loop ).ToList(), out min, out max );
	}

	static Vector3 Plan( bool alongX, float span, float across, float height )
	{
		return alongX ? new Vector3( across, span, height ) : new Vector3( span, across, height );
	}

	static Vector3 AcrossAxis( bool alongX )
	{
		return alongX ? new Vector3( 1f, 0f, 0f ) : new Vector3( 0f, 1f, 0f );
	}

	static Vector3 SpanAxis( bool alongX )
	{
		return alongX ? new Vector3( 0f, 1f, 0f ) : new Vector3( 1f, 0f, 0f );
	}

	static void JoinedSlab(
		ArchMesh canvas,
		IReadOnlyList<List<Vector3>> faces,
		Vector3 lift,
		ArchBrush brush,
		IReadOnlyList<(Vector3 From, Vector3 To)> openEdges = null )
	{
		if ( lift.IsNearZeroLength )
		{
			return;
		}

		var boundaries = new Dictionary<(long, long, long, long, long, long), (Vector3 From, Vector3 To, int Count)>();
		var openings = openEdges?
			.Select( edge => EdgeKey( edge.From, edge.To ) )
			.ToHashSet() ?? new HashSet<(long, long, long, long, long, long)>();

		using ( canvas.Welding() )
		{
			foreach ( var face in faces )
			{
				var corners = ArchSlab.Cleaned( face );

				if ( corners.Count < 3 )
				{
					continue;
				}

				if ( Vector3.Dot( ArchMesh.Newell( corners ), lift ) < 0f )
				{
					corners.Reverse();
				}

				canvas.Polygon( corners, brush, true );
				canvas.Polygon( corners.Select( point => point + lift ).ToList(), brush );

				for ( var index = 0; index < corners.Count; index++ )
				{
					var from = corners[index];
					var to = corners[(index + 1) % corners.Count];
					var key = EdgeKey( from, to );

					if ( boundaries.TryGetValue( key, out var boundary ) )
					{
						boundaries[key] = (boundary.From, boundary.To, boundary.Count + 1);
						continue;
					}

					boundaries[key] = (from, to, 1);
				}
			}

			foreach ( var boundary in boundaries.Values.Where( boundary => boundary.Count == 1 ) )
			{
				if ( openings.Contains( EdgeKey( boundary.From, boundary.To ) ) )
				{
					continue;
				}

				canvas.Quad( boundary.From, boundary.To, boundary.To + lift, boundary.From + lift, brush );
			}
		}
	}

	static (long, long, long, long, long, long) EdgeKey( Vector3 first, Vector3 second )
	{
		var a = PointKey( first );
		var b = PointKey( second );

		if ( ComesAfter( a, b ) )
		{
			(a, b) = (b, a);
		}

		return (a.X, a.Y, a.Z, b.X, b.Y, b.Z);
	}

	static (long X, long Y, long Z) PointKey( Vector3 point )
	{
		var size = ArchGridService.FinestSize;

		return (
			(long)MathF.Round( point.x / size ),
			(long)MathF.Round( point.y / size ),
			(long)MathF.Round( point.z / size ) );
	}

	static bool ComesAfter( (long X, long Y, long Z) first, (long X, long Y, long Z) second )
	{
		if ( first.X != second.X )
		{
			return first.X > second.X;
		}

		if ( first.Y != second.Y )
		{
			return first.Y > second.Y;
		}

		return first.Z > second.Z;
	}
}