editor/AdvancedMappingTools/Architecture/MarketStallEditor.cs

Editor tool that generates a wooden market stall prop. Settings control width, depth, roof style, crate count and seed; the builder lays out posts, rails, counter, display tiers, crates with produce and a sign board.

namespace AdvancedMappingTools;

public enum MarketRoofStyle
{
	Flat,
	Gable,
	[Title( "Striped Awning" )] Striped
}

public sealed class MarketStallSettings : PropSettings
{
	[Property, Group( "Market Stall" ), Range( 48, 512, slider: false ), WideMode]
	public float Width { get; set; } = 128.0f;

	[Property, Group( "Market Stall" ), Range( 48, 256, slider: false ), WideMode]
	public float Depth { get; set; } = 80.0f;

	[Property, Group( "Market Stall" ), Title( "Roof" ), WideMode]
	public MarketRoofStyle Roof { get; set; } = MarketRoofStyle.Striped;

	[Property, Group( "Market Stall" ), Title( "Crates" ), Range( 0, 24, slider: false ), WideMode, Description( "Crates of produce on the counter and the display." )]
	public int Crates { get; set; } = 8;

	[Property, Group( "Market Stall" ), Range( 0, 9999, slider: false ), WideMode, Description( "Seed for the crate sizes and the heaps of produce." )]
	public int Seed { get; set; } = 1;

	[Property, Group( "Market Stall" ), Title( "Sign Board" ), WideMode, Description( "A name board hanging under the front rail." )]
	public bool SignBoard { get; set; } = true;

	[Property, Group( "Market Stall" ), Title( "Display Tiers" ), WideMode, Description( "A stepped display at the back." )]
	public bool Tiers { get; set; } = true;

	public override string PropName => "Market Stall";
}

/// <summary>
/// A wooden market stall, open to -Y. Four posts, lower at the front, carry rails and a striped
/// awning with a valance, a sloped board roof or a gable. A counter with a front panel, a stepped
/// display at the back, crates with heaps of produce and a hanging name board. No part reaches
/// into another.
/// </summary>
public sealed class MarketStallGenerator : PropGenerator
{
	public override string Id => "marketstall";
	public override string Title => "Market Stall";
	public override Type SettingsType => typeof( MarketStallSettings );

	const float Post = 3.0f;
	public const float FrontEave = 84.0f;
	const float BackEave = 96.0f;
	const float Rail = 3.0f;
	public const float CounterTop = 34.0f;
	const float CounterDepth = 24.0f;

	protected override void Build( PropSettings settings, PropBuilder b )
	{
		var s = (MarketStallSettings)settings;
		var w = MathF.Max( s.Width, 48.0f ) * 0.5f;
		var d = MathF.Max( s.Depth, 48.0f ) * 0.5f;
		var gable = s.Roof == MarketRoofStyle.Gable;
		var back = gable ? FrontEave : BackEave;
		var stream = b.Context.Stream( "prop", $"stall{s.Seed}" );

		// Posts and rails.
		foreach ( var (name, x, y, h) in new[] { ("fl", -w, -d, FrontEave), ("fr", w - Post, -d, FrontEave), ("bl", -w, d - Post, back), ("br", w - Post, d - Post, back) } )
			b.Box( $"post_{name}", "Post", b.Main, new Vector3( x, y, 0.0f ), new Vector3( x + Post, y + Post, h ) );

		b.Box( "rail_front", "Rail", b.Main, new Vector3( -w, -d, FrontEave ), new Vector3( w, -d + Post, FrontEave + Rail ) );
		b.Box( "rail_back", "Rail", b.Main, new Vector3( -w, d - Post, back ), new Vector3( w, d, back + Rail ) );
		foreach ( var (name, x) in new[] { ("left", -w), ("right", w - Post) } )
			b.Part( $"rail_{name}", "Rail", b.Main, kit => kit.Hex(
				new Vector3( x, -d + Post, FrontEave ), new Vector3( x + Post, -d + Post, FrontEave ), new Vector3( x + Post, d - Post, back ), new Vector3( x, d - Post, back ),
				new Vector3( x, -d + Post, FrontEave + Rail ), new Vector3( x + Post, -d + Post, FrontEave + Rail ), new Vector3( x + Post, d - Post, back + Rail ), new Vector3( x, d - Post, back + Rail ) ) );

		Roof( s, b, w, d, back );

		// The counter: a front panel and a top between the front posts, a back board under the top.
		var inner = w - Post;
		b.Box( "counter_front", "Counter Front", b.Main, new Vector3( -inner, -d + 0.75f, 0.0f ), new Vector3( inner, -d + 2.25f, CounterTop - 2.0f ) );
		b.Box( "counter_back", "Counter Back", b.Main, new Vector3( -inner, -d + CounterDepth - 2.0f, 0.0f ), new Vector3( inner, -d + CounterDepth - 0.5f, CounterTop - 2.0f ) );
		b.Box( "counter_top", "Counter Top", b.Main, new Vector3( -inner, -d, CounterTop - 2.0f ), new Vector3( inner, -d + CounterDepth, CounterTop ) );

		var shelves = new List<(float Z, float Y0, float Y1)> { (CounterTop, -d + 1.0f, -d + CounterDepth - 1.0f) };
		if ( s.Tiers && d * 2.0f > CounterDepth + 40.0f )
		{
			b.Box( "tier_low", "Display", b.Main, new Vector3( -inner, d - Post - 24.0f, 0.0f ), new Vector3( inner, d - Post, 28.0f ) );
			b.Box( "tier_high", "Display", b.Main, new Vector3( -inner, d - Post - 12.0f, 28.0f ), new Vector3( inner, d - Post, 42.0f ) );
			shelves.Add( (28.0f, d - Post - 23.0f, d - Post - 13.0f) );
			shelves.Add( (42.0f, d - Post - 11.0f, d - Post - 1.0f) );
		}

		Crates( s, b, inner, shelves, stream );

		if ( s.SignBoard )
		{
			var half = w * 0.35f;
			b.Box( "sign", "Name Board", b.Detail, new Vector3( -half, -d, FrontEave - 12.0f ), new Vector3( half, -d + 1.0f, FrontEave - 3.0f ) );
			foreach ( var (name, x) in new[] { ("left", -half + 4.0f), ("right", half - 4.0f) } )
				b.Box( $"sign_hook_{name}", "Hook", b.Metal, new Vector3( x - 0.4f, -d + 0.3f, FrontEave - 3.0f ), new Vector3( x + 0.4f, -d + 0.7f, FrontEave ) );
		}
	}

	/// <summary>
	/// The roof lies on the rails. A striped awning reaches out over the front with a valance.
	/// </summary>
	static void Roof( MarketStallSettings s, PropBuilder b, float w, float d, float back )
	{
		var low = FrontEave + Rail;
		var high = back + Rail;
		// The roof plane clears the back rail: it is lifted by what the slope gains over the rail depth.
		float Z( float y ) => low + (high - low) * (y + d + Rail) / (2.0f * d);

		if ( s.Roof == MarketRoofStyle.Gable )
		{
			var ridge = low + 24.0f;
			foreach ( var (name, y0, y1) in new[] { ("front", -d - 10.0f, 0.0f), ("back", d + 10.0f, 0.0f) } )
			{
				// Each panel rests on its rail and rises to the ridge over the middle.
				float Rise( float y ) => low + (ridge - low) * (1.0f - MathF.Abs( y ) / d);
				var a = y0; var c = y1;
				b.Part( $"roof_{name}", "Roof Panel", b.Detail, kit =>
				{
					var (p0, p1) = a < c ? (a, c) : (c, a);
					var z0 = Rise( p0 ); var z1 = Rise( p1 );
					kit.Hex( new Vector3( -w - 6, p0, z0 ), new Vector3( w + 6, p0, z0 ), new Vector3( w + 6, p1, z1 ), new Vector3( -w - 6, p1, z1 ),
						new Vector3( -w - 6, p0, z0 + 1.0f ), new Vector3( w + 6, p0, z0 + 1.0f ), new Vector3( w + 6, p1, z1 + 1.0f ), new Vector3( -w - 6, p1, z1 + 1.0f ) );
				} );
			}
			// The gable panels touch under the ridge; a ridge board covers the seam.
			b.Box( "ridge", "Ridge Board", b.Main, new Vector3( -w - 6, -1.5f, ridge + 1.0f ), new Vector3( w + 6, 1.5f, ridge + 2.0f ) );
			// Ridge posts carry the middle.
			foreach ( var (name, x) in new[] { ("left", -w), ("right", w - Post) } )
				b.Box( $"ridge_post_{name}", "Ridge Post", b.Main, new Vector3( x, -1.5f, low ), new Vector3( x + Post, 1.5f, low + (ridge - low) * (1.0f - 1.5f / d) - 0.01f ) );
			return;
		}

		var front = -d - (s.Roof == MarketRoofStyle.Striped ? 14.0f : 6.0f);
		var rear = d + 2.0f;
		var strips = s.Roof == MarketRoofStyle.Striped ? Math.Max( 3, (int)MathF.Round( w * 2.0f / 16.0f ) ) : 1;
		var span = (w * 2.0f + 8.0f) / strips;

		for ( var k = 0; k < strips; k++ )
		{
			var x0 = -w - 4.0f + k * span;
			var x1 = x0 + span;
			var material = k % 2 == 0 ? b.Detail : b.Main;
			b.Part( $"roof{k}", s.Roof == MarketRoofStyle.Striped ? "Awning Strip" : "Roof", material, kit =>
				kit.Hex( new Vector3( x0, front, Z( front ) ), new Vector3( x1, front, Z( front ) ), new Vector3( x1, rear, Z( rear ) ), new Vector3( x0, rear, Z( rear ) ),
					new Vector3( x0, front, Z( front ) + 0.5f ), new Vector3( x1, front, Z( front ) + 0.5f ), new Vector3( x1, rear, Z( rear ) + 0.5f ), new Vector3( x0, rear, Z( rear ) + 0.5f ) ) );

			if ( s.Roof == MarketRoofStyle.Striped )
				b.Box( $"valance{k}", "Valance", material, new Vector3( x0, front - 0.5f, Z( front ) - 8.0f ), new Vector3( x1, front, Z( front ) + 0.5f ) );
		}
	}

	/// <summary>
	/// Crates with a heap of produce, side by side on the counter and the display steps.
	/// </summary>
	static void Crates( MarketStallSettings s, PropBuilder b, float inner, List<(float Z, float Y0, float Y1)> shelves, SeedStream stream )
	{
		var left = Math.Clamp( s.Crates, 0, 24 );
		var n = 0;
		foreach ( var (z, y0, y1) in shelves )
		{
			var x = -inner + 1.5f;
			while ( left > 0 )
			{
				var w = stream.Range( 12.0f, 18.0f );
				if ( x + w > inner - 1.5f ) break;
				var depth = MathF.Min( y1 - y0, stream.Range( 9.0f, 12.0f ) );
				var cy = (y0 + y1) * 0.5f;
				var h = stream.Range( 4.0f, 6.0f );
				var cx = x + w * 0.5f;
				b.Box( $"crate{n}", "Crate", b.Main, new Vector3( x, cy - depth * 0.5f, z ), new Vector3( x + w, cy + depth * 0.5f, z + h ) );
				var r = MathF.Min( w, depth ) * 0.5f - 0.5f;
				var heap = stream.Range( 0.4f, 0.7f ) * r;
				b.Part( $"produce{n}", "Produce", b.Detail, kit => kit.Lathe( new Vector3( cx, cy, 0.0f ),
					[new( 0, z + h ), new( r, z + h ), new( r * 0.85f, z + h + heap * 0.55f ), new( r * 0.5f, z + h + heap * 0.9f ), new( 0, z + h + heap )], 12 ) );
				x += w + stream.Range( 1.0f, 4.0f );
				left--;
				n++;
			}
		}
	}
}

/// <summary>
/// Places a market stall. One click, the counter faces -Y.
/// </summary>
[ToolCategory( "Street" )]
[Title( "Market Stall" ), Icon( "storefront" )]
public sealed class MarketStallEditor( PrimitiveTool tool ) : PropEditor( tool )
{
	readonly MarketStallGenerator _generator = new();

	public MarketStallSettings Settings { get; } = new();

	protected override IPartGenerator Generator => _generator;

	public override PropSettings Props => Settings;
}