Editor/Roof/ArchRoofCapping.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static partial class ArchRoofGen {
	// By exact name: a kit saved before the cap existed just gets no cap.
	static void Capping( ArchMesh canvas, List<(Vector3 From, Vector3 To)> creases, float thickness, ArchKit kit, ArchBrush brush, ArchPlan plan, int level, int hostId ) {
		var profile = ArchProfiles.Named( kit, "ridge_cap" );

		if ( profile is null ) {
			return;
		}

		var lift = Vector3.Up * thickness;
		var reach = MathF.Max( 1f, profile.Max.x );
		var junctions = Junctions( creases );

		// Corners stop short (one block takes them); collinear bands overlap; hip low ends stop at the eave.
		foreach ( var crease in creases ) {
			var along = (crease.To - crease.From).Normal;
			var flat = MathF.Abs( crease.To.z - crease.From.z ) < 0.1f;

			var from = crease.From + lift + along * (Meets( junctions, crease.From ) ? reach * 0.6f : flat ? -reach * 0.75f : 0f);
			var to = crease.To + lift - along * (Meets( junctions, crease.To ) ? reach * 0.6f : -reach * 0.75f);

			if ( (to - from).Length < 1f ) {
				continue;
			}

			foreach ( var span in ArchCut.Outside( plan, kit, level, hostId,
				new Vector2( from.x, from.y ), new Vector2( to.x, to.y ),
				MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), ArchCutAffects.Roofs ) ) {
				canvas.Extrude( new List<Vector3> { Vector3.Lerp( from, to, span.From ), Vector3.Lerp( from, to, span.To ) }, profile, 1f, Rotation.Identity, brush );
			}
		}

		foreach ( var node in junctions ) {
			var corner = new Vector3( reach, reach, 0f );
			var seat = node + lift;

			// The bands round it break over a cut, so the block that takes their corner has to go with them -
			// left standing it is a cube floating in mid-air over the hole, attached to nothing.
			if ( ArchCut.Covers( plan, kit, level, hostId, seat + Vector3.Up * profile.Min.y, seat + Vector3.Up * profile.Max.y, ArchCutAffects.Roofs ) ) {
				continue;
			}

			canvas.Box(
				seat + Vector3.Up * profile.Min.y - corner,
				seat + Vector3.Up * profile.Max.y + corner,
				brush );
		}
	}

	// Same-direction ends at one spot are one band split, not a corner - left to overlap.
	static List<Vector3> Junctions( List<(Vector3 From, Vector3 To)> creases ) {
		var ends = new List<(Vector3 At, Vector3 Along)>();

		foreach ( var crease in creases ) {
			var along = (crease.To - crease.From).Normal;

			ends.Add( (crease.From, along) );
			ends.Add( (crease.To, along) );
		}

		var junctions = new List<Vector3>();

		foreach ( var end in ends ) {
			var meeting = ends.Where( other => (other.At - end.At).Length < 1f ).ToList();

			if ( meeting.Count < 2 || meeting.All( other => MathF.Abs( Vector3.Dot( other.Along, end.Along ) ) > 0.99f ) ) {
				continue;
			}

			if ( !junctions.Any( node => (node - end.At).Length < 1f ) ) {
				junctions.Add( end.At );
			}
		}

		return junctions;
	}

	static bool Meets( List<Vector3> junctions, Vector3 point ) {
		return junctions.Any( node => (node - point).Length < 1f );
	}

	// A flat deck's only capping is the flashing where it dies into a taller section.
	static List<(Vector3 From, Vector3 To)> Abutted( List<List<Vector2>> loops, Func<Vector2, Vector2, bool> abuts, float height ) {
		var lines = new List<(Vector3 From, Vector3 To)>();

		foreach ( var loop in loops ) {
			var blocked = ArchBrokenRun.Blocked( loop, abuts );

			for ( var index = 0; index < loop.Count; index++ ) {
				var from = loop[index];
				var to = loop[(index + 1) % loop.Count];

				if ( blocked[index] && (to - from).Length > 4f ) {
					lines.Add( (new Vector3( from.x, from.y, height ), new Vector3( to.x, to.y, height )) );
				}
			}
		}

		return lines;
	}
}