editor/AdvancedMappingTools/Geometry/RoofExpansion.cs

Editor partial class for AdvancedMappingTools' RoofEditor. Adds the roof overhang, edge trim profile, gutter and downpipe settings, and builds the overhang bounds plus the edge-trim mesh (square, stepped, drip profiles) for flat, gabled, cross-gable, dome and conical roofs.

namespace AdvancedMappingTools;
public enum RoofEdgeProfile
{
	Square,
	Stepped,
	Drip
}

public sealed partial class RoofEditor
{
	[Property, Group( "Roof Overhangs" ), Title( "Separate Four Sides" ), WideMode]
	public bool SeparateOverhangs { get; set; }

	[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
	public float StartOverhang { get; set; } = 8;

	[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
	public float FinishOverhang { get; set; } = 8;

	[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
	public float FrontOverhang { get; set; } = 8;

	[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
	public float BackOverhang { get; set; } = 8;

	[Property, Group( "Roof Edge" ), WideMode]
	public bool EdgeTrim { get; set; }

	[Property, Group( "Roof Edge" ), WideMode]
	public RoofEdgeProfile EdgeProfile { get; set; }

	[Property, Group( "Roof Edge" ), Range( .25f, 32, slider: false ), WideMode]
	public float EdgeTrimDepth { get; set; } = 4;

	[Property, Group( "Roof Edge" ), Title( "Edge Material" ), WideMode]
	public Material EdgeTrimSurface { get; set; }

	[Property, Group( "Gutters" ), WideMode]
	public bool FrontGutter { get; set; } = true;

	[Property, Group( "Gutters" ), WideMode]
	public bool BackGutter { get; set; } = true;

	[Property, Group( "Drainage" ), Title( "Front Downpipes" ), WideMode]
	public bool FrontDownpipes { get; set; } = true;

	[Property, Group( "Drainage" ), Title( "Back Downpipes" ), WideMode]
	public bool BackDownpipes { get; set; } = true;

	[Property, Group( "Drainage" ), WideMode]
	public bool ManualDownpipePosition { get; set; }

	[Property, Group( "Drainage" ), Title( "Downpipe Along Edge (%)" ), Range( 0, 100, slider: false ), WideMode]
	public float DownpipeAlong { get; set; } = 50;

	void RoofBounds( out float x0, out float x1, out float y0, out float y1 )
	{
		var ex = EndOverhang < 0 ? Overhang : EndOverhang;
		var ey = EaveOverhang < 0 ? Overhang : EaveOverhang;
		var m = MathF.Max( WallMargin, 0 );
		x0 = -(SeparateOverhangs ? Math.Clamp( StartOverhang, 0, 256 ) : ex) - m;
		x1 = RectLength + (SeparateOverhangs ? Math.Clamp( FinishOverhang, 0, 256 ) : ex) + m;
		y0 = -(SeparateOverhangs ? Math.Clamp( FrontOverhang, 0, 256 ) : ey) - m;
		y1 = RectDepth + (SeparateOverhangs ? Math.Clamp( BackOverhang, 0, 256 ) : ey) + m;
	}

	void AddEdgeTrim( MeshKit kit, Transform frame, float x0, float x1, float y0, float y1 )
	{
		if ( !EdgeTrim )
			return;
		var destination = kit;
		kit = new MeshKit
		{
			Frame = frame,
			LiftFlushFaces = false
		};
		kit.Material = EdgeTrimSurface ?? TrimMaterial;
		void FinishTrim()
		{
			var trim = kit.Finish();
			if ( trim == null )
				return;
			var bodyKit = new MeshKit
			{
				LiftFlushFaces = false
			};
			if ( Hollow && Shape != RoofShape.Flat )
				BuildHollowRoof( bodyKit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, RoofInteriorSurface ?? TrimMaterial, CustomProfile );
			else
				BuildRoof( bodyKit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
			var body = bodyKit.Finish();
			var clean = MeshCutter.SubtractClean( trim, body, Transform.Zero, EdgeTrimSurface ?? TrimMaterial );
			destination.ImportMesh( clean ?? trim, Transform.Zero );
		}

		var d = Math.Clamp( EdgeTrimDepth, .25f, 32 );
		var skin = MathF.Min( d * .25f, 2 );
		if ( Shape == RoofShape.CrossGable )
		{
			var source = new MeshKit
			{
				LiftFlushFaces = false
			};
			if ( Hollow )
				BuildHollowRoof( source, Transform.Zero, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
			else
				BuildRoof( source, Transform.Zero, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
			var body = source.Finish();
			foreach ( var face in body.FaceHandles )
			{
				var vertices = body.GetFaceVertices( face ).Select( body.GetVertexPosition ).ToArray();
				if ( vertices.Length < 3 )
					continue;
				var normal = Vector3.Zero;
				for ( var i = 1; i + 1 < vertices.Length && normal.Length < .001f; i++ )
					normal = Vector3.Cross( vertices[i] - vertices[0], vertices[i + 1] - vertices[0] ).Normal;
				if ( MathF.Abs( normal.y ) < .7f )
					continue;
				var center = vertices.Aggregate( Vector3.Zero, ( a, b ) => a + b ) / vertices.Length;
				if ( MathF.Min( MathF.Abs( center.y - y0 ), MathF.Abs( center.y - y1 ) ) > MathF.Max( ShellThickness * 2, 2 ) )
					continue;
				var outward = new Vector3( 0, MathF.Sign( normal.y ), 0 );
				var rings = new List<Vector3[]>
				{
					vertices,
					vertices.Select( v => v + outward * skin ).ToArray()
				};
				if ( EdgeProfile != RoofEdgeProfile.Square )
					rings.Add( vertices.Select( v => center + (v - center) * .8f + outward * d + (EdgeProfile == RoofEdgeProfile.Drip ? Vector3.Down * skin * 2 : Vector3.Zero) ).ToArray() );
				kit.BeginPart();
				kit.Face( rings[0].Reverse().ToArray() );
				kit.Face( rings[^1] );
				for ( var i = 0; i + 1 < rings.Count; i++ )
					for ( var j = 0; j < vertices.Length; j++ )
					{
						var k = (j + 1) % vertices.Length;
						kit.Face( rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j] );
					}
			}

			destination.ImportMesh( kit.Finish(), Transform.Zero );
			return;
		}

		if ( Shape is RoofShape.Dome or RoofShape.Conical )
		{
			var rx = (x1 - x0) * .5f;
			var ry = (y1 - y0) * .5f;
			var radial = skin / MathF.Min( rx, ry );
			kit.Frame = frame.ToWorld( new Transform( new Vector3( (x0 + x1) * .5f, (y0 + y1) * .5f, 0 ), Rotation.Identity, new Vector3( rx, ry, 1 ) ) );
			kit.Ring( Vector3.Zero, 1 + radial, 1, EdgeThickness - d, EdgeThickness, 64 );
			if ( EdgeProfile != RoofEdgeProfile.Square )
				kit.Ring( Vector3.Zero, 1 + d / MathF.Min( rx, ry ), 1 + radial, EdgeThickness - d, EdgeThickness - d + skin, 64 );
			if ( EdgeProfile == RoofEdgeProfile.Drip )
				kit.Ring( Vector3.Zero, 1 + d / MathF.Min( rx, ry ), 1 + (d - skin) / MathF.Min( rx, ry ), EdgeThickness - d - skin * 2, EdgeThickness - d, 64 );
			kit.Frame = frame;
			FinishTrim();
			return;
		}

		var sampleKit = new MeshKit
		{
			LiftFlushFaces = false
		};
		BuildRoof( sampleKit, Transform.Zero, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
		var sample = sampleKit.Finish();
		Vector2[] profile = EdgeProfile switch
		{
			RoofEdgeProfile.Square => [new( 0, -d ), new( skin, -d ), new( skin, 0 ), new( 0, 0 )],
			RoofEdgeProfile.Stepped => [new( 0, -d ), new( d, -d ), new( d, -d + skin ), new( skin, -d + skin ), new( skin, 0 ), new( 0, 0 )],
			_ => [new( 0, -d ), new( d - skin, -d ), new( d - skin, -d - skin * 2 ), new( d, -d - skin * 2 ), new( d, -d + skin ), new( skin, -d + skin ), new( skin, 0 ), new( 0, 0 )]
		};
		foreach ( var side in new[]
		{
			-1,
			1
		}

 )
		{
			var y = side < 0 ? y0 : y1;
			var cross = profile.Select( p => new Vector2( p.x * side, p.y ) ).ToArray();
			if ( side < 0 )
				Array.Reverse( cross );
			var rings = new List<Vector3[]>();
			void Finish()
			{
				if ( rings.Count < 2 )
				{
					rings.Clear();
					return;
				}

				kit.BeginPart();
				kit.Face( rings[0].Reverse().ToArray() );
				kit.Face( rings[^1] );
				for ( var i = 0; i + 1 < rings.Count; i++ )
					for ( var j = 0; j < cross.Length; j++ )
					{
						var k = (j + 1) % cross.Length;
						kit.Face( rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j] );
					}

				rings.Clear();
			}

			for ( var i = 0; i <= 32; i++ )
			{
				var x = x0 + (x1 - x0) * i / 32;
				var z = HouseDetailGeometry.Height( sample, new( x, y ) );
				if ( !float.IsFinite( z ) )
				{
					Finish();
					continue;
				}

				rings.Add( cross.Select( p => new Vector3( x, y + p.x, z + p.y ) ).ToArray() );
			}

			Finish();
		}

		FinishTrim();
	}
}