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.
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 ) );
}
}