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

namespace Sunless.Architecture;

// Hip edges run at 45 degrees, which no rectilinear grid can hold - so cuts run per face.
// The rim is not emitted here: ArchRoofGen.JoinedSlab already walls every edge one face owns.
static class ArchFaceCarve {
	const float Grain = ArchCarve.Grain;

	public static List<List<Vector3>> Surround( IReadOnlyList<List<Vector3>> faces, IReadOnlyList<ArchCarveVolume> cuts, float thickness, float contact = 0f ) {
		if ( cuts is not { Count: > 0 } ) {
			return faces.ToList();
		}

		var kept = new List<List<Vector3>>();

		foreach ( var face in faces ) {
			var pieces = new List<List<Vector3>> { new( face ) };

			foreach ( var cut in cuts ) {
				pieces = Subtract( pieces, cut, thickness, contact );
			}

			kept.AddRange( pieces );
		}

		// A cut landing on a hip line strands a corner on the face nothing cut.
		Conform( kept );

		return kept;
	}

	static List<List<Vector3>> Subtract( List<List<Vector3>> pieces, ArchCarveVolume cut, float thickness, float contact ) {
		var footprint = ArchFootprint.Wind( cut.Footprint.ToList() );

		if ( footprint.Count < 3 ) {
			return pieces;
		}

		var bounds = Bounds( footprint, cut, thickness, contact ).ToList();
		var kept = new List<List<Vector3>>();

		foreach ( var piece in pieces ) {
			var inside = piece;
			var outside = new List<List<Vector3>>();

			for ( var index = 0; index < bounds.Count && inside is not null; index++ ) {
				Split( inside, bounds[index], out inside, out var beyond );

				if ( beyond is not null ) {
					outside.Add( beyond );
				}
			}

			if ( inside is null ) {
				kept.Add( piece );
				continue;
			}

			kept.AddRange( outside );
		}

		return kept;
	}

	// A deck RAKES, so the band is a boundary drawn across the face and not a yes or no about the whole of
	// it: the footprint's edges say where the cut stands in plan, and its floor and ceiling say where the
	// deck has climbed out of it. Judging by the overlap's extremes took a whole hip slope out for touching
	// the eave, and left the ridge over a shaft that plainly reached it.
	static IEnumerable<Func<Vector3, float>> Bounds( IReadOnlyList<Vector2> footprint, ArchCarveVolume cut, float thickness, float contact ) {
		for ( var index = 0; index < footprint.Count; index++ ) {
			var from = footprint[index];
			var to = footprint[(index + 1) % footprint.Count];

			yield return point => Cross( from, to, point );
		}

		yield return point => cut.Ceiling.At( Flat( point ) ) - point.z + contact;
		yield return point => point.z + thickness - cut.Floor.At( Flat( point ) );
	}

	static Vector2 Flat( Vector3 point ) => new( point.x, point.y );

	// Sutherland-Hodgman on one boundary: signed depth per corner, crossing interpolated in three.
	static void Split( List<Vector3> piece, Func<Vector3, float> depth, out List<Vector3> inside, out List<Vector3> outside ) {
		var within = new List<Vector3>();
		var beyond = new List<Vector3>();

		for ( var index = 0; index < piece.Count; index++ ) {
			var current = piece[index];
			var next = piece[(index + 1) % piece.Count];
			var here = depth( current );
			var there = depth( next );

			if ( here >= -Grain ) within.Add( current );
			if ( here <= Grain ) beyond.Add( current );

			if ( here > Grain && there < -Grain || here < -Grain && there > Grain ) {
				var crossing = current + (next - current) * (here / (here - there));

				within.Add( crossing );
				beyond.Add( crossing );
			}
		}

		inside = Real( within ) ? within : null;
		outside = Real( beyond ) ? beyond : null;
	}

	// A stranded corner must be inserted - a T left in becomes a wall across the deck.
	static void Conform( List<List<Vector3>> pieces ) {
		var corners = pieces.SelectMany( piece => piece ).ToList();

		foreach ( var piece in pieces ) {
			for ( var index = piece.Count - 1; index >= 0; index-- ) {
				var from = piece[index];
				var to = piece[(index + 1) % piece.Count];
				var along = corners
					.Where( corner => Between( from, to, corner ) )
					.OrderBy( corner => (corner - from).Length )
					.ToList();

				for ( var step = along.Count - 1; step > 0; step-- ) {
					if ( (along[step] - along[step - 1]).Length < Grain ) {
						along.RemoveAt( step );
					}
				}

				piece.InsertRange( index + 1, along );
			}
		}
	}

	static bool Between( Vector3 from, Vector3 to, Vector3 point ) {
		var span = to - from;
		var length = span.Length;

		if ( length < Grain ) {
			return false;
		}

		var along = Vector3.Dot( point - from, span ) / length;

		if ( along < Grain || along > length - Grain ) {
			return false;
		}

		return (from + span * (along / length) - point).Length < Grain;
	}

	static float Cross( Vector2 a, Vector2 b, Vector3 point ) {
		var span = b - a;
		var length = span.Length;

		if ( length < 0.0001f ) {
			return 0f;
		}

		return (span.x * (point.y - a.y) - span.y * (point.x - a.x)) / length;
	}

	// Narrower than anything the grid can author is clip noise, not deck. Measured against the FINEST grid
	// step rather than the grain: a band boundary crossing a rake almost level with it leaves a hairline
	// ribbon the length of the roof, and a ribbon is a face whose texture axis smears down the slope.
	static bool Real( List<Vector3> loop ) {
		if ( loop.Count < 3 ) {
			return false;
		}

		var flat = loop.Select( point => new Vector2( point.x, point.y ) ).ToList();
		var area = MathF.Abs( ArchFootprint.SignedArea( flat ) );
		var longest = 0f;

		for ( var index = 0; index < flat.Count; index++ ) {
			longest = MathF.Max( longest, (flat[(index + 1) % flat.Count] - flat[index]).Length );
		}

		return longest > ArchGridService.FinestSize && area / longest > ArchGridService.FinestSize;
	}
}