editor/AdvancedMappingTools/Geometry/LineBuilders.cs

Editor-side set of primitive builders for AdvancedMappingTools. Each class derives from LineEditor/BuilderEditor and generates mesh geometry between two clicked points: ramp, bridge, pier, planks, ladder, balcony, tower and dome.

Self Modifying CodeFile Access
namespace AdvancedMappingTools;

/// <summary>
/// Shapes that span between two clicked points.
/// </summary>
public abstract class LineEditor( PrimitiveTool tool ) : BuilderEditor( tool )
{
	protected override int MinPoints => 2;
	protected override int MaxPoints => 2;

	protected Vector3 Start => Points[0].Position;
	protected Vector3 End => Points[1].Position;
	protected float Length => Start.WithZ( 0.0f ).Distance( End.WithZ( 0.0f ) );
	protected Rotation Facing => Rotation.LookAt( (End - Start).WithZ( 0.0f ).Normal, Vector3.Up );
	protected Transform LineFrame => new( Start, Facing );

	protected override bool IsShapeValid => Length > 1.0f;
}

/// <summary>
/// A sloped ramp from the first point up to the second.
/// </summary>
[ToolCategory( "Stairs & Ramps" )]
[Title( "Ramp" ), Icon( "signal_cellular_0_bar" )]
public sealed class RampEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	[Property, Group( "Ramp" ), Range( 1, 2048, slider: false ), WideMode]
	public float Height { get; set; } = 64.0f;

	[Property, Group( "Ramp" ), Range( 4, 2048, slider: false ), WideMode]
	public float Width { get; set; } = 96.0f;

	[Property, Group( "Ramp" ), Title( "Side Rails" ), WideMode]
	public bool SideRails { get; set; } = false;

	protected override void BuildMesh( MeshKit kit )
	{
		var half = Width * 0.5f;
		var length = Length;

		kit.Frame = LineFrame;
		kit.Profile( [new( 0, 0 ), new( length, 0 ), new( length, Height )], -half, half );

		if ( !SideRails ) return;

		kit.Material = TrimMaterial;

		foreach ( var y in new[] { -half - 1.0f, half + 1.0f } )
		{
			kit.Beam( new Vector3( 0.0f, y, 36.0f ), new Vector3( length, y, Height + 36.0f ), 2.0f, 2.0f );
			kit.Beam( new Vector3( 0.0f, y, 0.0f ), new Vector3( 0.0f, y, 36.0f ), 2.0f, 2.0f );
			kit.Beam( new Vector3( length, y, Height ), new Vector3( length, y, Height + 36.0f ), 2.0f, 2.0f );
		}
	}
}

/// <summary>
/// A bridge deck with an optional hump, railings and piers.
/// </summary>
[ToolCategory( "Tunnels & Bridges" )]
[Title( "Bridge" ), Icon( "architecture" )]
public sealed class BridgeEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	const int Segments = 16;

	[Property, Group( "Bridge" ), Range( 8, 2048, slider: false ), WideMode]
	public float Width { get; set; } = 96.0f;

	[Property, Group( "Bridge" ), Range( 1, 256, slider: false ), WideMode]
	public float Thickness { get; set; } = 8.0f;

	[Property, Group( "Bridge" ), Range( 0, 1024, slider: false ), WideMode, Description( "How far the middle of the deck rises." )]
	public float Hump { get; set; } = 24.0f;

	[Property, Group( "Bridge" ), Title( "Railing Height" ), Range( 0, 128, slider: false ), WideMode]
	public float RailingHeight { get; set; } = 36.0f;

	[Property, Group( "Bridge" ), Range( 0, 32, slider: false ), WideMode]
	public int Piers { get; set; } = 2;

	[Property, HideIf( nameof( Piers ), 0 ), Group( "Bridge" ), Title( "Pier Depth" ), Range( 1, 2048, slider: false ), WideMode]
	public float PierDepth { get; set; } = 128.0f;

	[Property, HideIf( nameof( Piers ), 0 ), Advanced, Group( "Bridge" ), Title( "Piers To Ground" ), WideMode, Description( "Extends every pier down to the first surface below it. A pier without a hit keeps its Pier Depth." )]
	public bool PiersToGround { get; set; } = false;

	[Property, Group( "Banks" ), Title( "Follow Banks" ), WideMode, Description( "The deck runs from the height of the first click to the height of the second, so it can join two banks of different height." )]
	public bool FollowBanks { get; set; } = true;

	[Property, Group( "Banks" ), Title( "Match Road Width" ), WideMode, Description( "An end placed on the end of a road, sidewalk or terrain path takes over its width." )]
	public bool MatchRoadWidth { get; set; } = true;

	protected override bool Connects => true;

	// Each end keeps the height it was put at, that is what lets the deck climb.
	protected override bool AllowsHeight => FollowBanks;
	protected override bool KeepsHeight( Vector3 point ) => true;

	static readonly HashSet<string> Roads = [nameof( RoadEditor ), nameof( SidewalkEditor ), nameof( TerrainPathEditor )];

	/// <summary>
	/// An end on the end of a finished road lands there and takes its width. The second end also
	/// takes the height of what was clicked, so the deck can climb from bank to bank.
	/// </summary>
	protected override Vector3 Dock( Vector3 point )
	{
		var docked = base.Dock( point );

		if ( Points.Count == 1 && FollowBanks && CursorHit() is Vector3 hit ) docked = docked.WithZ( hit.z );

		if ( !ConnectFinished || SceneEditorSession.Active is null ) return docked;

		if ( LineSnap.NearestEnd( SceneEditorSession.Active.Scene, docked, DockRadius, Roads ) is not { } road ) return docked;

		if ( MatchRoadWidth ) Width = road.Width;

		DrawLabel( road.End, "Road end", Color.Green );

		return FollowBanks || Points.Count == 0 ? road.End : road.End.WithZ( Start.z );
	}

	float Climb => FollowBanks && Points.Count > 1 ? End.z - Start.z : 0.0f;

	float DeckHeight( float t ) => Hump * MathF.Sin( MathF.PI * t ) + Climb * t;

	protected override void BuildMesh( MeshKit kit )
	{
		var length = Length;
		var half = Width * 0.5f;
		var outline = new List<Vector2>();

		for ( var i = 0; i <= Segments; i++ )
		{
			var t = i / (float)Segments;
			outline.Add( new Vector2( length * t, DeckHeight( t ) - Thickness ) );
		}

		for ( var i = Segments; i >= 0; i-- )
		{
			var t = i / (float)Segments;
			outline.Add( new Vector2( length * t, DeckHeight( t ) ) );
		}

		kit.Frame = LineFrame;
		kit.Profile( outline, -half, half );

		kit.Material = TrimMaterial;

		for ( var p = 1; p <= Piers; p++ )
		{
			var t = p / (Piers + 1f);
			var x = length * t;
			var bottom = -PierDepth;

			if ( PiersToGround && SceneEditorSession.Active is not null )
			{
				var scene = SceneEditorSession.Active.Scene;
				var top = DeckHeight( t ) - Thickness;
				var world = LineFrame.PointToWorld( new Vector3( x, 0.0f, top ) );
				var hit = scene.Trace.Ray( world + Vector3.Up * 64.0f, world + Vector3.Down * MathF.Max( PierDepth * 4.0f, 512.0f ) ).Run();

				if ( hit.Hit ) bottom = MathF.Min( bottom, LineFrame.PointToLocal( hit.EndPosition ).z );
			}

			kit.Box( new Vector3( x - 8.0f, -half, bottom ), new Vector3( x + 8.0f, half, DeckHeight( t ) - Thickness ) );
		}

		if ( RailingHeight <= 0.0f ) return;

		foreach ( var y in new[] { -half + 1.0f, half - 1.0f } )
		{
			for ( var i = 0; i < Segments; i++ )
			{
				var t0 = i / (float)Segments;
				var t1 = (i + 1) / (float)Segments;
				var a = new Vector3( length * t0, y, DeckHeight( t0 ) );
				var b = new Vector3( length * t1, y, DeckHeight( t1 ) );

				kit.Beam( a + Vector3.Up * RailingHeight, b + Vector3.Up * RailingHeight, 2.0f, 2.0f );
				kit.Beam( a, a + Vector3.Up * RailingHeight, 1.5f, 1.5f );
			}
		}
	}
}

/// <summary>
/// A wooden jetty: deck planks on pilings.
/// </summary>
[ToolCategory( "Tunnels & Bridges" )]
[Title( "Pier" ), Icon( "deck" )]
public sealed class PierEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	[Property, Group( "Pier" ), Range( 8, 1024, slider: false ), WideMode]
	public float Width { get; set; } = 72.0f;

	[Property, Group( "Pier" ), Title( "Plank Width" ), Range( 2, 64, slider: false ), WideMode]
	public float PlankWidth { get; set; } = 8.0f;

	[Property, Group( "Pier" ), Title( "Plank Gap" ), Range( 0, 8, slider: false ), WideMode]
	public float PlankGap { get; set; } = 0.5f;

	[Property, Group( "Pier" ), Title( "Piling Spacing" ), Range( 16, 512, slider: false ), WideMode]
	public float PilingSpacing { get; set; } = 96.0f;

	[Property, Group( "Pier" ), Title( "Piling Depth" ), Range( 8, 1024, slider: false ), WideMode]
	public float PilingDepth { get; set; } = 96.0f;

	protected override void BuildMesh( MeshKit kit )
	{
		var length = Length;
		var half = Width * 0.5f;
		var step = MathF.Max( PlankWidth, 2.0f ) + PlankGap;

		kit.Frame = LineFrame;

		for ( var x = 0.0f; x + PlankWidth <= length; x += step )
			kit.Box( new Vector3( x, -half, -2.0f ), new Vector3( x + PlankWidth, half, 0.0f ) );

		kit.Material = TrimMaterial;

		kit.Box( new Vector3( 0.0f, -half, -8.0f ), new Vector3( length, -half + 4.0f, -2.0f ) );
		kit.Box( new Vector3( 0.0f, half - 4.0f, -8.0f ), new Vector3( length, half, -2.0f ) );

		for ( var x = 4.0f; x < length; x += MathF.Max( PilingSpacing, 16.0f ) )
		{
			kit.Cylinder( new Vector3( x, -half + 2.0f, 0.0f ), 4.0f, 4.0f, -PilingDepth, 8.0f, 8 );
			kit.Cylinder( new Vector3( x, half - 2.0f, 0.0f ), 4.0f, 4.0f, -PilingDepth, 8.0f, 8 );
		}
	}
}

/// <summary>
/// Planks nailed across a gap, for barricaded doors and windows, or laid flat as a floor.
/// </summary>
[ToolCategory( "Props" )]
[Title( "Planks" ), Icon( "reorder" )]
public sealed class PlanksEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Trim" };

	[Property, Group( "Planks" ), Range( 1, 64, slider: false ), WideMode]
	public int Count { get; set; } = 5;

	[Property, Group( "Planks" ), Title( "Plank Width" ), Range( 1, 64, slider: false ), WideMode]
	public float PlankWidth { get; set; } = 8.0f;

	[Property, Group( "Planks" ), Range( 0.25f, 16, slider: false ), WideMode]
	public float Thickness { get; set; } = 2.0f;

	[Property, Group( "Planks" ), Range( 0, 64, slider: false ), WideMode]
	public float Gap { get; set; } = 3.0f;

	[Property, Group( "Planks" ), Range( -2048, 2048, slider: false ), WideMode, Description( "Height of the lowest plank above the drawn line." )]
	public float Elevation { get; set; } = 16.0f;

	[Property, Group( "Planks" ), Title( "Max Tilt" ), Range( 0, 45, slider: false ), WideMode]
	public float MaxTilt { get; set; } = 8.0f;

	[Property, Group( "Planks" ), Title( "Lay Flat" ), WideMode]
	public bool LayFlat { get; set; } = false;

	[Property, Group( "Planks" ), Range( 0, 9999, slider: false ), WideMode]
	public int Seed { get; set; } = 1;

	protected override void BuildMesh( MeshKit kit )
	{
		var random = new Random( Seed );
		var length = Length;
		var step = PlankWidth + Gap;

		kit.Frame = LineFrame;

		for ( var i = 0; i < Count; i++ )
		{
			var tilt = MathF.Tan( ((float)random.NextDouble() * 2.0f - 1.0f) * MaxTilt.DegreeToRadian() ) * length * 0.5f;
			var offset = step * i;

			if ( LayFlat )
				kit.Beam( new Vector3( 0.0f, offset - tilt, Elevation ), new Vector3( length, offset + tilt, Elevation ), PlankWidth, Thickness );
			else
				kit.Beam( new Vector3( 0.0f, 0.0f, Elevation + offset - tilt ), new Vector3( length, 0.0f, Elevation + offset + tilt ), Thickness, PlankWidth );
		}
	}
}

/// <summary>
/// A ladder from the first point, leaning towards the second.
/// </summary>
[ToolCategory( "Stairs & Ramps" )]
[Title( "Ladder" ), Icon( "format_list_numbered" )]
public sealed class LadderEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	[Property, Group( "Ladder" ), Range( 16, 2048, slider: false ), WideMode]
	public float Height { get; set; } = 128.0f;

	[Property, Group( "Ladder" ), Range( 8, 128, slider: false ), WideMode]
	public float Width { get; set; } = 24.0f;

	[Property, Group( "Ladder" ), Title( "Rung Spacing" ), Range( 2, 64, slider: false ), WideMode]
	public float RungSpacing { get; set; } = 12.0f;

	protected override void BuildMesh( MeshKit kit )
	{
		var top = new Vector3( Length, 0.0f, Height );
		var half = Width * 0.5f;
		var side = new Vector3( 0.0f, half, 0.0f );
		var rungs = (int)MathF.Floor( top.Length / MathF.Max( RungSpacing, 2.0f ) );

		kit.Frame = LineFrame;
		kit.Beam( -side, top - side, 2.0f, 2.0f );
		kit.Beam( side, top + side, 2.0f, 2.0f );

		kit.Material = TrimMaterial;

		for ( var i = 1; i < rungs; i++ )
		{
			var point = top * (i / (float)rungs);
			kit.Beam( point - side, point + side, 1.0f, 1.0f );
		}
	}
}

public enum BalconyStyle
{
	[Title( "Bars" )] Bars,
	[Title( "Glass" )] Glass,
	[Title( "Solid" )] Solid,
	[Title( "Open" )] Open
}

/// <summary>
/// A balcony on a wall: slab, railing and brackets. It sticks out to the left of the line.
/// </summary>
[ToolCategory( "Doors & Windows" )]
[Title( "Balcony" ), Icon( "balcony" )]
public sealed class BalconyEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	[Property, Group( "Balcony" ), Range( 8, 512, slider: false ), WideMode]
	public float Depth { get; set; } = 48.0f;

	[Property, Group( "Balcony" ), Range( -2048, 2048, slider: false ), WideMode]
	public float Elevation { get; set; } = 128.0f;

	[Property, Group( "Balcony" ), Range( 1, 64, slider: false ), WideMode]
	public float Thickness { get; set; } = 6.0f;

	[Property, Group( "Balcony" ), Title( "Railing Height" ), Range( 8, 128, slider: false ), WideMode]
	public float RailingHeight { get; set; } = 36.0f;

	[Property, Group( "Balcony" ), Title( "Post Spacing" ), Range( 2, 128, slider: false ), WideMode]
	public float PostSpacing { get; set; } = 6.0f;

	[Property, Group( "Balcony" ), Title( "Style" ), WideMode, Description( "Bars, a glass parapet, a solid parapet or an open slab." )]
	public BalconyStyle Style { get; set; } = BalconyStyle.Bars;

	[Property, Group( "Balcony" ), Title( "Brackets" ), WideMode]
	public bool Brackets { get; set; } = true;

	[Property, ShowIf( nameof( Style ), BalconyStyle.Glass ), Group( "Materials" ), Title( "Glass" ), WideMode, Description( "Material of the glass parapet. Empty uses the trim material." )]
	public Material GlassMaterial { get; set; }


	protected override void BuildMesh( MeshKit kit )
	{
		var length = Length;
		var z = Elevation;
		var top = z + Thickness;
		var rail = top + RailingHeight;
		var spacing = MathF.Max( PostSpacing, 2.0f );

		kit.Frame = LineFrame;
		kit.Box( new Vector3( 0.0f, 0.0f, z ), new Vector3( length, Depth, top ) );

		void AddBrackets()
		{
			if ( !Brackets ) return;

			kit.Beam( new Vector3( 4.0f, 0.0f, z - Depth * 0.6f ), new Vector3( 4.0f, Depth * 0.8f, z ), 3.0f, 3.0f );
			kit.Beam( new Vector3( length - 4.0f, 0.0f, z - Depth * 0.6f ), new Vector3( length - 4.0f, Depth * 0.8f, z ), 3.0f, 3.0f );
		}

		if ( Style == BalconyStyle.Open )
		{
			AddBrackets();
			return;
		}

		kit.Material = TrimMaterial;

		kit.Box( new Vector3( 0.0f, Depth - 1.0f, rail ), new Vector3( length, Depth + 1.0f, rail + 2.0f ) );
		kit.Box( new Vector3( -1.0f, 0.0f, rail ), new Vector3( 1.0f, Depth, rail + 2.0f ) );
		kit.Box( new Vector3( length - 1.0f, 0.0f, rail ), new Vector3( length + 1.0f, Depth, rail + 2.0f ) );

		var step = Style == BalconyStyle.Bars ? spacing : MathF.Max( spacing * 4.0f, 8.0f );

		for ( var x = 0.0f; x <= length; x += step )
			kit.Box( new Vector3( x - 0.75f, Depth - 0.75f, top ), new Vector3( x + 0.75f, Depth + 0.75f, rail ) );

		for ( var y = step; y < Depth; y += step )
		{
			kit.Box( new Vector3( -0.75f, y - 0.75f, top ), new Vector3( 0.75f, y + 0.75f, rail ) );
			kit.Box( new Vector3( length - 0.75f, y - 0.75f, top ), new Vector3( length + 0.75f, y + 0.75f, rail ) );
		}

		if ( Style == BalconyStyle.Bars )
		{
			for ( var x = 0.0f; x <= length; x += spacing )
				kit.Box( new Vector3( x - 0.4f, Depth - 0.4f, top ), new Vector3( x + 0.4f, Depth + 0.4f, rail ) );

			for ( var y = spacing; y < Depth; y += spacing )
			{
				kit.Box( new Vector3( -0.4f, y - 0.4f, top ), new Vector3( 0.4f, y + 0.4f, rail ) );
				kit.Box( new Vector3( length - 0.4f, y - 0.4f, top ), new Vector3( length + 0.4f, y + 0.4f, rail ) );
			}
		}
		else if ( Style == BalconyStyle.Glass )
		{
			kit.Material = GlassMaterial ?? TrimMaterial;
			kit.Box( new Vector3( 0.0f, Depth - 0.15f, top + 2.0f ), new Vector3( length, Depth + 0.15f, rail - 1.0f ) );
			kit.Box( new Vector3( -0.15f, 0.0f, top + 2.0f ), new Vector3( 0.15f, Depth, rail - 1.0f ) );
			kit.Box( new Vector3( length - 0.15f, 0.0f, top + 2.0f ), new Vector3( length + 0.15f, Depth, rail - 1.0f ) );
		}
		else
		{
			kit.Box( new Vector3( 0.0f, Depth - 2.0f, top ), new Vector3( length, Depth, rail - 1.0f ) );
			kit.Box( new Vector3( -2.0f, 0.0f, top ), new Vector3( 0.0f, Depth, rail - 1.0f ) );
			kit.Box( new Vector3( length, 0.0f, top ), new Vector3( length + 2.0f, Depth, rail - 1.0f ) );
		}

		AddBrackets();
	}
}

/// <summary>
/// A round tower. The first point is the centre, the second sets the radius.
/// </summary>
[ToolCategory( "Columns & Towers" )]
[Title( "Tower" ), Icon( "castle" )]
public sealed class TowerEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	public enum TowerRoof
	{
		Flat,
		Cone,
		Dome,
		Battlements
	}

	[Property, Group( "Tower" ), Range( 16, 8192, slider: false ), WideMode]
	public float Height { get; set; } = 384.0f;

	[Property, Group( "Tower" ), Range( 4, 64, slider: false ), WideMode]
	public int Sides { get; set; } = 24;

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

	[Property, ShowIf( nameof( Hollow ), true ), Group( "Tower" ), Title( "Wall Thickness" ), Range( 1, 256, slider: false ), WideMode]
	public float WallThickness { get; set; } = 16.0f;

	[Property, ShowIf( nameof( Hollow ), true ), Group( "Tower" ), Title( "Inner Floors" ), Range( 0, 32, slider: false ), WideMode]
	public int InnerFloors { get; set; } = 3;

	[Property, Group( "Tower" ), WideMode]
	public TowerRoof Roof { get; set; } = TowerRoof.Cone;

	[Property, Group( "Tower" ), Title( "Roof Height" ), Range( 1, 2048, slider: false ), WideMode]
	public float RoofHeight { get; set; } = 160.0f;

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

	protected override void BuildMesh( MeshKit kit )
	{
		var radius = Length;
		var sides = Math.Clamp( Sides, 4, 64 );
		var inner = MathF.Max( radius - WallThickness, 0.5f );
		var center = Vector3.Zero;

		kit.Frame = LineFrame;

		if ( Hollow ) kit.Ring( center, radius, inner, 0.0f, Height, sides );
		else kit.Cylinder( center, radius, radius, 0.0f, Height, sides );

		kit.Material = TrimMaterial;

		if ( Hollow )
		{
			for ( var f = 0; f <= InnerFloors; f++ )
			{
				var z = Height * f / (InnerFloors + 1f);
				kit.Cylinder( center, inner, inner, z - 4.0f, z, sides );
			}
		}

		kit.Material = RoofMaterial ?? TrimMaterial;

		switch ( Roof )
		{
			case TowerRoof.Flat:
				kit.Cylinder( center, radius + 4.0f, radius + 4.0f, Height, Height + 8.0f, sides );
				break;

			case TowerRoof.Cone:
				kit.Cylinder( center, radius + 12.0f, 0.0f, Height, Height + RoofHeight, sides );
				break;

			case TowerRoof.Dome:
				var dome = new List<Vector2>();
				for ( var i = 0; i <= 8; i++ )
				{
					var angle = MathF.PI * 0.5f * i / 8.0f;
					dome.Add( new Vector2( MathF.Cos( angle ) * radius, Height + MathF.Sin( angle ) * RoofHeight ) );
				}
				dome[^1] = dome[^1] with { x = 0.0f };
				dome.Insert( 0, new Vector2( 0.0f, Height ) );
				kit.Lathe( center, dome, sides );
				break;

			case TowerRoof.Battlements:
				var merlons = sides / 2;
				for ( var i = 0; i < merlons; i++ )
				{
					var a0 = MathF.PI * 2.0f * i / merlons;
					var a1 = a0 + MathF.PI / merlons;

					Vector3 At( float angle, float r, float z ) => new( MathF.Cos( angle ) * r, MathF.Sin( angle ) * r, z );

					kit.Hex( At( a0, inner, Height ), At( a0, radius, Height ), At( a1, radius, Height ), At( a1, inner, Height ),
						At( a0, inner, Height + 24.0f ), At( a0, radius, Height + 24.0f ), At( a1, radius, Height + 24.0f ), At( a1, inner, Height + 24.0f ) );
				}
				break;
		}
	}
}

/// <summary>
/// A dome on a drum. The first point is the centre, the second sets the radius.
/// </summary>
[ToolCategory( "Roofs" )]
[Title( "Dome" ), Icon( "panorama_photosphere" )]
public sealed class DomeEditor( PrimitiveTool tool ) : LineEditor( tool )
{
	[Property, Group( "Dome" ), Title( "Drum Height" ), Range( 0, 2048, slider: false ), WideMode]
	public float DrumHeight { get; set; } = 48.0f;

	[Property, Group( "Dome" ), Title( "Dome Height" ), Range( 4, 2048, slider: false ), WideMode]
	public float DomeHeight { get; set; } = 128.0f;

	[Property, Group( "Dome" ), Range( 2, 32, slider: false ), WideMode]
	public int Rings { get; set; } = 10;

	[Property, Group( "Dome" ), Range( 6, 64, slider: false ), WideMode]
	public int Sides { get; set; } = 24;

	public enum DomePattern
	{
		Smooth,
		Ribbed,
		Scales
	}

	[Property, Group( "Dome" ), WideMode]
	public DomePattern Pattern { get; set; } = DomePattern.Smooth;

	[Property, HideIf( nameof( Pattern ), DomePattern.Smooth ), Group( "Dome" ), Title( "Pattern Depth" ), Range( 0.25f, 32, slider: false ), WideMode]
	public float PatternDepth { get; set; } = 3.0f;

	[Property, Group( "Dome" ), Title( "Cornice" ), WideMode, Description( "A moulded ring at the top of the drum." )]
	public bool Cornice { get; set; } = true;

	[Property, Group( "Dome" ), Title( "Lantern" ), WideMode, Description( "A small turret on top." )]
	public bool Lantern { get; set; } = true;

	[Property, Group( "Dome" ), Title( "Cross" ), WideMode, Description( "A cross on the very top." )]
	public bool Cross { get; set; } = true;

	Vector3 OnDome( float radius, float ring, float side, float lift = 0.0f )
	{
		var up = MathF.PI * 0.5f * ring / Rings;
		var around = MathF.PI * 2.0f * side / Math.Clamp( Sides, 6, 64 );
		var flat = MathF.Cos( up ) * (radius + lift);

		return new Vector3( MathF.Cos( around ) * flat, MathF.Sin( around ) * flat, DrumHeight + MathF.Sin( up ) * (DomeHeight + lift) );
	}

	protected override void BuildMesh( MeshKit kit )
	{
		var radius = Length;
		var sides = Math.Clamp( Sides, 6, 64 );
		var rings = Math.Clamp( Rings, 2, 32 );
		var profile = new List<Vector2> { new( 0.0f, 0.0f ), new( radius, 0.0f ) };

		for ( var i = 0; i <= rings; i++ )
		{
			var angle = MathF.PI * 0.5f * i / rings;
			profile.Add( new Vector2( i == rings ? 0.0f : MathF.Cos( angle ) * radius, DrumHeight + MathF.Sin( angle ) * DomeHeight ) );
		}

		kit.Frame = LineFrame;
		kit.Lathe( Vector3.Zero, profile, sides );

		kit.Material = TrimMaterial;

		if ( Pattern == DomePattern.Scales )
		{
			// Staggered diamonds, each a small pyramid sitting on the smooth shell.
			kit.Material = SurfaceMaterial;

			for ( var ring = 1; ring < rings - 1; ring++ )
			{
				for ( var side = 0; side < sides; side++ )
				{
					var s = side + (ring % 2) * 0.5f;
					var low = OnDome( radius, ring - 1, s, 0.1f );
					var right = OnDome( radius, ring, s + 0.5f, 0.1f );
					var high = OnDome( radius, ring + 1, s, 0.1f );
					var left = OnDome( radius, ring, s - 0.5f, 0.1f );
					var peak = OnDome( radius, ring, s, PatternDepth );

					kit.BeginPart();
					kit.Face( low, right, peak );
					kit.Face( right, high, peak );
					kit.Face( high, left, peak );
					kit.Face( left, low, peak );
					kit.Face( low, left, high, right );
				}
			}

			kit.Material = TrimMaterial;
		}
		else if ( Pattern == DomePattern.Ribbed )
		{
			for ( var side = 0; side < sides; side += 2 )
			{
				for ( var ring = 0; ring < rings - 1; ring++ )
					kit.Beam( OnDome( radius, ring, side, PatternDepth * 0.5f ), OnDome( radius, ring + 1, side, PatternDepth * 0.5f ), PatternDepth * 1.5f, PatternDepth );
			}
		}

		if ( Cornice && DrumHeight > 0.0f )
		{
			kit.Lathe( Vector3.Zero, [new( 0.0f, DrumHeight - 12.0f ), new( radius, DrumHeight - 12.0f ), new( radius + 4.0f, DrumHeight - 8.0f ), new( radius + 4.0f, DrumHeight - 4.0f ), new( radius + 10.0f, DrumHeight ), new( radius, DrumHeight + 4.0f ), new( 0.0f, DrumHeight + 4.0f )], sides );
		}

		if ( !Lantern && !Cross ) return;

		var top = DrumHeight + DomeHeight;
		var size = radius * 0.12f;
		var tip = top;

		if ( Lantern )
		{
			kit.Cylinder( Vector3.Zero, size * 1.2f, size * 1.2f, top - 2.0f, top + size * 0.5f, 12 );
			kit.Cylinder( Vector3.Zero, size, size, top + size * 0.5f, top + size * 2.0f, 12 );
			kit.Cylinder( Vector3.Zero, size * 1.3f, 0.0f, top + size * 2.0f, top + size * 3.0f, 12 );
			tip = top + size * 3.0f;
		}

		if ( !Cross ) return;

		var height = radius * 0.4f;
		var bar = MathF.Max( radius * 0.012f, 0.75f );

		kit.Box( new Vector3( -bar, -bar, tip - 2.0f ), new Vector3( bar, bar, tip + height ) );
		kit.Box( new Vector3( -bar, -height * 0.35f, tip + height * 0.65f ), new Vector3( bar, height * 0.35f, tip + height * 0.65f + bar * 2.0f ) );
	}
}