Editor tool that generates a bus shelter prop. Defines settings (length, depth, side walls, bench, sign, ad panel, bin) and builds the geometry from boxes, a hex canopy, glass panes, a bench, a bin and a stop sign.
namespace AdvancedMappingTools;
public sealed class BusShelterSettings : PropSettings
{
[Property, Group( "Bus Shelter" ), Range( 48, 512, slider: false ), WideMode, Description( "Length along the street." )]
public float Length { get; set; } = 160.0f;
[Property, Group( "Bus Shelter" ), Title( "Side Walls" ), WideMode, Description( "Glass walls at both ends." )]
public bool SideWalls { get; set; } = true;
[Property, Group( "Bus Shelter" ), WideMode]
public bool Bench { get; set; } = true;
[Property, Group( "Bus Shelter" ), Title( "Stop Sign" ), WideMode, Description( "A pole with the stop sign and a timetable next to the shelter." )]
public bool Sign { get; set; } = true;
[Property, Group( "Bus Shelter" ), Title( "Advert Panel" ), WideMode, Description( "A lit advertising case as one end wall." )]
public bool AdPanel { get; set; } = true;
[Property, Group( "Bus Shelter" ), Title( "Bin" ), WideMode]
public bool Bin { get; set; } = true;
[Property, Group( "Bus Shelter" ), Range( 32, 96, slider: false ), WideMode, Description( "Depth from the back wall to the front edge of the roof frame." )]
public float Depth { get; set; } = 48.0f;
public override string PropName => "Bus Shelter";
}
/// <summary>
/// A bus shelter on a concrete pad. Square posts carry eave beams and a sloped canopy, glass sits
/// between the posts at the back and the ends, one end can be an advertising case. A slatted bench
/// on two feet, a bin, and a stop sign with a timetable on its own pole. The open side faces +Y,
/// towards the street. No part reaches into another.
/// </summary>
public sealed class BusShelterGenerator : PropGenerator
{
public override string Id => "busshelter";
public override string Title => "Bus Shelter";
public override Type SettingsType => typeof( BusShelterSettings );
public const float Height = 96.0f;
const float Pad = 2.0f;
const float PostX = 1.0f;
const float PostY = 1.5f;
const float Beam = 3.0f;
const float Pane = 0.25f;
const float Rail = 2.0f;
protected override void Build( PropSettings settings, PropBuilder b )
{
var s = (BusShelterSettings)settings;
var length = MathF.Max( s.Length, 48.0f );
var depth = Math.Clamp( s.Depth, 32.0f, 96.0f );
var half = length * 0.5f;
var beamLow = Height - Beam;
var glassLow = Pad + Rail;
b.Box( "pad", "Concrete Pad", b.Main, new Vector3( -half - 24.0f, -6.0f, 0.0f ), new Vector3( half + 24.0f, depth + 8.0f, Pad ) );
// Posts: along the back at most 64 apart, and one at each front corner.
var bays = Math.Max( 1, (int)MathF.Ceiling( length / 64.0f ) );
var backPosts = Enumerable.Range( 0, bays + 1 ).Select( k => -half + length * k / bays ).ToList();
foreach ( var (k, x) in backPosts.Select( ( x, k ) => (k, x) ) )
b.Box( $"post_back{k}", "Post", b.Metal, new Vector3( x - PostX, 0.0f, Pad ), new Vector3( x + PostX, PostY * 2.0f, beamLow ) );
foreach ( var (name, x) in new[] { ("left", -half), ("right", half) } )
b.Box( $"post_front_{name}", "Post", b.Metal, new Vector3( x - PostX, depth - PostY * 2.0f, Pad ), new Vector3( x + PostX, depth, beamLow ) );
// Eave beams: back and front over the full length, the ends between them.
b.Box( "beam_back", "Eave Beam", b.Metal, new Vector3( -half - PostX, 0.0f, beamLow ), new Vector3( half + PostX, PostY * 2.0f, Height ) );
b.Box( "beam_front", "Eave Beam", b.Metal, new Vector3( -half - PostX, depth - PostY * 2.0f, beamLow ), new Vector3( half + PostX, depth, Height ) );
foreach ( var (name, x) in new[] { ("left", -half), ("right", half) } )
b.Box( $"beam_{name}", "Eave Beam", b.Metal, new Vector3( x - PostX, PostY * 2.0f, beamLow ), new Vector3( x + PostX, depth - PostY * 2.0f, Height ) );
// The canopy lies on the beams and falls towards the back, with a fascia along the front.
b.Part( "roof", "Canopy", b.Metal, kit =>
kit.Hex( new Vector3( -half - 6.0f, -6.0f, Height ), new Vector3( half + 6.0f, -6.0f, Height ), new Vector3( half + 6.0f, depth + 10.0f, Height ), new Vector3( -half - 6.0f, depth + 10.0f, Height ),
new Vector3( -half - 6.0f, -6.0f, Height + 2.5f ), new Vector3( half + 6.0f, -6.0f, Height + 2.5f ), new Vector3( half + 6.0f, depth + 10.0f, Height + 5.0f ), new Vector3( -half - 6.0f, depth + 10.0f, Height + 5.0f ) ) );
b.Box( "fascia", "Fascia", b.Detail, new Vector3( -half - 6.0f, depth + 10.0f, Height - 2.0f ), new Vector3( half + 6.0f, depth + 11.0f, Height + 5.0f ) );
// Back glass with a rail under each pane.
for ( var k = 0; k + 1 < backPosts.Count; k++ )
{
var x0 = backPosts[k] + PostX;
var x1 = backPosts[k + 1] - PostX;
b.Box( $"rail_back{k}", "Bottom Rail", b.Metal, new Vector3( x0, PostY - 1.0f, Pad ), new Vector3( x1, PostY + 1.0f, glassLow ) );
b.Box( $"glass_back{k}", "Back Glass", b.Detail, new Vector3( x0, PostY - Pane, glassLow ), new Vector3( x1, PostY + Pane, beamLow ) );
}
// The ends: glass, or on the left an advertising case standing between the posts.
foreach ( var (name, x, ad) in new[] { ("left", -half, s.AdPanel), ("right", half, false) } )
{
if ( !s.SideWalls && !ad ) continue;
var y0 = PostY * 2.0f;
var y1 = depth - PostY * 2.0f;
if ( ad )
{
b.Box( $"ad_case_{name}", "Advert Case", b.Metal, new Vector3( x - 3.0f, y0, Pad ), new Vector3( x + 3.0f, y1, beamLow ) );
b.Box( $"ad_face_{name}", "Advert", b.Detail, new Vector3( x + 3.0f, y0 + 2.0f, Pad + 8.0f ), new Vector3( x + 3.2f, y1 - 2.0f, beamLow - 4.0f ) );
b.Box( $"ad_back_{name}", "Advert", b.Detail, new Vector3( x - 3.2f, y0 + 2.0f, Pad + 8.0f ), new Vector3( x - 3.0f, y1 - 2.0f, beamLow - 4.0f ) );
continue;
}
b.Box( $"rail_{name}", "Bottom Rail", b.Metal, new Vector3( x - 1.0f, y0, Pad ), new Vector3( x + 1.0f, y1, glassLow ) );
b.Box( $"glass_{name}", "Side Glass", b.Detail, new Vector3( x - Pane, y0, glassLow ), new Vector3( x + Pane, y1, beamLow ) );
}
if ( s.Bench ) Bench( b, MathF.Min( length * 0.6f, 120.0f ) * 0.5f );
if ( s.Bin ) b.Round( "bin", "Bin", b.Metal, -half - 15.0f, depth - 6.0f, 6.0f, 6.5f, Pad, Pad + 30.0f, 16 );
if ( s.Sign ) Sign( b, half + 16.0f, depth + 2.0f );
}
/// <summary>
/// Three slats on two pedestal feet with a bracket each, in front of the back glass.
/// </summary>
static void Bench( PropBuilder b, float half )
{
const float seat = 17.0f;
const float front = 21.0f;
const float back = 6.0f;
foreach ( var (name, x) in new[] { ("left", -half + 6.0f), ("right", half - 6.0f) } )
{
b.Box( $"bench_foot_{name}", "Bench Foot", b.Metal, new Vector3( x - 1.0f, 10.0f, Pad ), new Vector3( x + 1.0f, 14.0f, seat - 1.5f ) );
b.Box( $"bench_bracket_{name}", "Bench Bracket", b.Metal, new Vector3( x - 1.0f, back, seat - 1.5f ), new Vector3( x + 1.0f, front, seat ) );
}
var slat = (front - back - 1.0f) / 3.0f;
for ( var k = 0; k < 3; k++ )
{
var y0 = back + k * (slat + 0.5f);
b.Box( $"bench_slat{k}", "Bench Slat", b.Main, new Vector3( -half, y0, seat ), new Vector3( half, y0 + slat, seat + 1.25f ) );
}
}
/// <summary>
/// A pole with a round stop sign and a timetable case on its street side.
/// </summary>
static void Sign( PropBuilder b, float x, float y )
{
// The pole stands on the pad.
b.Round( "sign_pole", "Sign Pole", b.Metal, x, y, 1.25f, 1.0f, Pad, 108.0f, 12 );
b.Disc( "sign", "Stop Sign", b.Detail, x, 100.0f, 9.0f, y + 1.35f, y + 1.85f, 24 );
b.Box( "timetable", "Timetable", b.Detail, new Vector3( x - 6.0f, y + 1.35f, 58.0f ), new Vector3( x + 6.0f, y + 2.5f, 78.0f ) );
}
}
/// <summary>
/// Places a bus shelter. One click, the open side faces the street.
/// </summary>
[ToolCategory( "Street" )]
[Title( "Bus Shelter" ), Icon( "directions_bus" )]
public sealed class BusShelterEditor( PrimitiveTool tool ) : PropEditor( tool )
{
readonly BusShelterGenerator _generator = new();
public BusShelterSettings Settings { get; } = new();
protected override IPartGenerator Generator => _generator;
public override PropSettings Props => Settings;
}