Editor/Data/ArchProfileCatalog.cs

An editor-side catalog of architectural cross-section profiles. It implements IArchCatalog and returns a set of named ArchProfile objects with precomputed 2D point lists for shapes like gutters, pipes, pillars, balusters and other profiles used by the extruder/geometry generator.

File Access
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

// The one shelf core stands itself, and for the same reason it stands the select subtool: a gutter belongs to roofs,
// a downpipe to pipes, a handrail and its balusters to stairs, a shaft to pillars. No module authors sections, and
// the extruder that reads them is core's own - so this is neutral geometry wearing a catalog, not a leaf's catalog
// left behind.
public sealed class ArchProfileCatalog : IArchCatalog
{
	public ArchShelf Shelf => ArchShelf.Profiles;

	public string Named( object entry ) => entry is ArchProfile profile ? profile.Name : null;

	public IEnumerable<object> Shipped() => Sections();

	public IEnumerable<object> Authored() => Array.Empty<object>();

	public bool Save( object entry ) => false;

	public static List<ArchProfile> Sections()
	{
		return new List<ArchProfile>
		{
			// Extruder +x is inward on a CCW eave run, so the back wall sits on x=0.
			new() { Name = "gutter", Points = BoxGutter( 6f, 4.5f, 0.7f, 2f ) },
			new() { Name = "gutter_bracket", Points = Square( 1.1f ) },
			// Four points is all a capping tile needs at this scale.
			new()
			{
				Name = "ridge_cap",
				Points = new List<Vector2> { new( -4f, -2.5f ), new( -1f, 2f ), new( 1f, 2f ), new( 4f, -2.5f ) }
			},
			new() { Name = "cornice", Points = Cove( 5f, 4f, 4 ) },
			new() { Name = "downpipe", Points = Circle( 2.2f, 12 ) },
			new() { Name = "downpipe_square", Points = Square( 2.1f ) },
			new() { Name = "handrail", Points = Circle( 1.6f, 6 ) },
			// Lathe silhouettes, not sections: x is radius, y is height off the bottom rail.
			new() { Name = "baluster_turned", Closed = false, Points = TurnedBaluster() },
			new() { Name = "baluster_square", Closed = false, Points = new List<Vector2> { new( 1.35f, 0f ), new( 1.35f, 28f ) } },
			new()
			{
				Name = "baluster_colonial",
				Closed = false,
				Points = new List<Vector2>
				{
					new( 1.4f, 0f ), new( 1.4f, 4f ), new( 1f, 5.5f ), new( 1f, 21f ),
					new( 1.5f, 23f ), new( 1.4f, 24.5f ), new( 1.4f, 28f )
				}
			},
			new()
			{
				Name = "parapet_cap",
				Points = new List<Vector2> { new( -7f, 0f ), new( 7f, 0f ), new( 7f, 3f ), new( 0f, 5f ), new( -7f, 3f ) }
			},
			new()
			{
				Name = "fascia",
				Points = new List<Vector2> { new( -1f, -4f ), new( 1f, -4f ), new( 1f, 4f ), new( -1f, 4f ) }
			},
			new() { Name = "pipe", Points = Circle( 3f, 8 ) },
			new() { Name = "pillar_square", Points = Square( 10f ) },
			new() { Name = "pillar_round", Points = Circle( 10f, 12 ) },
			new() { Name = "pillar_octagon", Points = Circle( 10f, 8 ) },
			new() { Name = "pillar_cross", Points = Cross( 12f, 4.5f ) },
			new()
			{
				Name = "i_beam",
				Points = new List<Vector2>
				{
					new( -6f, -8f ), new( 6f, -8f ), new( 6f, -6f ), new( 1.5f, -6f ), new( 1.5f, 6f ),
					new( 6f, 6f ), new( 6f, 8f ), new( -6f, 8f ), new( -6f, 6f ), new( -1.5f, 6f ),
					new( -1.5f, -6f ), new( -6f, -6f )
				}
			}
		};
	}

	// The five diameters a turned baluster reads as from across a garden.
	static List<Vector2> TurnedBaluster()
	{
		return new List<Vector2>
		{
			new( 1.5f, 0f ), new( 1.75f, 7f ), new( 0.8f, 13.2f ),
			new( 1.28f, 17.6f ), new( 0.72f, 23f ), new( 1.45f, 28f )
		};
	}

	static List<Vector2> Circle( float radius, int segments )
	{
		return Enumerable.Range( 0, segments )
			.Select( index => index / (float)segments * MathF.Tau )
			.Select( angle => new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) ) * radius )
			.ToList();
	}

	// Wound clockwise so extruded normals face outward; the back wall tucks behind the drip edge.
	static List<Vector2> BoxGutter( float width, float depth, float thickness, float backRise )
	{
		return new List<Vector2>
		{
			new( 0f, backRise ),
			new( 0f, -depth ),
			new( -width, -depth ),
			new( -width, 0f ),
			new( -width + thickness, 0f ),
			new( -width + thickness, -depth + thickness ),
			new( -thickness, -depth + thickness ),
			new( -thickness, backRise )
		};
	}

	// Interior runs are CCW, so the extruder's +x points into the room.
	static List<Vector2> Cove( float reach, float drop, int segments )
	{
		var points = new List<Vector2> { new( 0f, 0f ), new( reach, 0f ) };

		for ( var index = 1; index < segments; index++ )
		{
			var angle = MathF.PI * 0.5f * index / segments;
			points.Add( new Vector2( reach * MathF.Cos( angle ), -drop * MathF.Sin( angle ) ) );
		}

		points.Add( new Vector2( 0f, -drop ) );

		return points;
	}

	static List<Vector2> Square( float half )
	{
		return new List<Vector2> { new( -half, -half ), new( half, -half ), new( half, half ), new( -half, half ) };
	}

	static List<Vector2> Cross( float arm, float half )
	{
		return new List<Vector2>
		{
			new( -half, -arm ), new( half, -arm ), new( half, -half ), new( arm, -half ),
			new( arm, half ), new( half, half ), new( half, arm ), new( -half, arm ),
			new( -half, half ), new( -arm, half ), new( -arm, -half ), new( -half, -half )
		};
	}
}