editor/AdvancedMappingTools/Architecture/PlaygroundEditor.cs

Editor tool that generates playground geometry on a rectangle: a picket fence with a front gate, benches, a slide tower, an A-frame swing, a sandbox, a climbing frame and a seesaw. Pieces are packed into rows with a seeded layout, and settings toggle each piece and pick wood, metal and sand materials.

namespace AdvancedMappingTools;
public sealed class PlaygroundSettings : GeneratorSettings
{
	public float Width { get; set; } = 320.0f;
	public float Depth { get; set; } = 240.0f;

	[Property, Group( "Playground" ), WideMode, Description( "A tower with a roof, a ladder and a slide." )]
	public bool Slide { get; set; } = true;

	[Property, Group( "Playground" ), WideMode, Description( "An A-frame with two seats on chains." )]
	public bool Swing { get; set; } = true;

	[Property, Group( "Playground" ), WideMode, Description( "A framed sandbox with seat boards and a bucket." )]
	public bool Sandbox { get; set; } = true;

	[Property, Group( "Playground" ), Title( "Climbing Frame" ), WideMode]
	public bool ClimbingFrame { get; set; } = true;

	[Property, Group( "Playground" ), WideMode, Description( "Park benches along the front, either side of the gate." )]
	public bool Benches { get; set; } = true;

	[Property, Group( "Playground" ), Range( 0, 9999, slider: false ), WideMode, Description( "Seed for the order and spacing of the pieces." )]
	public int Seed { get; set; } = 1;

	[Property, Group( "Materials" ), Title( "Wood" ), ResourceType( "vmat" ), WideMode]
	public string WoodMaterial { get; set; }

	[Property, Group( "Materials" ), Title( "Metal" ), ResourceType( "vmat" ), WideMode]
	public string MetalMaterial { get; set; }

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

	[Property, Group( "Playground" ), WideMode, Description( "A picket fence round the edge with a gate at the front." )]
	public bool Fence { get; set; } = true;

	[Property, ShowIf( nameof( Sandbox ), true ), Group( "Materials" ), Title( "Sand" ), ResourceType( "vmat" ), WideMode]
	public string SandMaterial { get; set; }
}

/// <summary>
/// A playground on the rectangle from (0, 0) to (Width, Depth), the gate at the front (y = 0).
/// A slide tower, an A-frame swing, a sandbox, a climbing frame and a seesaw are packed in rows
/// inside a picket fence, benches stand along the front. A piece that finds no room is left out.
/// No part reaches into another.
/// </summary>
public sealed class PlaygroundGenerator : IPartGenerator
{
	public string Id => "playground";
	public string Title => "Playground";
	public Type SettingsType => typeof( PlaygroundSettings );

	const string PlayCategory = "playground";
	public const float Margin = 12.0f;
	const float Spacing = 16.0f;
	public const float FenceHeight = 36.0f;
	public const float GateWidth = 36.0f;

	sealed record Piece( string Name, float Width, float Depth, Action<float, float> Build );

	public PartPlan Generate( GeneratorSettings settings, GenContext context )
	{
		var s = (PlaygroundSettings)settings;
		var root = context.Group( "root", "Playground", "Building", PlayCategory );
		var w = MathF.Max( s.Width, 64.0f );
		var d = MathF.Max( s.Depth, 64.0f );
		var wood = context.Material( s.WoodMaterial );
		var metal = context.Material( s.MetalMaterial );
		var sand = context.Material( s.SandMaterial ?? s.WoodMaterial );
		var stream = context.Stream( PlayCategory, $"layout{s.Seed}" );

		void Solid( string id, string name, string kind, Material m, Action<MeshKit> build ) =>
			root.Add( context.Solid( id, name, kind, PlayCategory, m, kit => { kit.LiftFlushFaces = false; build( kit ); } ) );

		if ( s.Fence ) Fence( Solid, wood, w, d );

		// Benches along the front with their backs to the fence, either side of the gate.
		var front = Margin;
		var bench = MathF.Min( 64.0f, w * 0.5f - GateWidth * 0.5f - Margin * 2.0f );
		if ( s.Benches && bench >= 32.0f && d >= Margin * 2.0f + 24.0f )
		{
			foreach ( var (k, cx) in new[] { w * 0.25f - GateWidth * 0.25f, w * 0.75f + GateWidth * 0.25f }.Select( ( x, k ) => (k, x) ) )
				Bench( Solid, wood, metal, $"bench{k}", cx - bench * 0.5f, cx + bench * 0.5f, Margin );
			front = Margin + 22.0f + Spacing;
		}

		var pieces = new List<Piece>();
		if ( s.ClimbingFrame ) pieces.Add( new( "climb", 76.0f, 76.0f, ( x, y ) => Climb( Solid, metal, x, y ) ) );
		var box = Math.Clamp( MathF.Min( w, d ) * 0.25f, 40.0f, 96.0f );
		if ( s.Sandbox ) pieces.Add( new( "sandbox", box + 4.0f, box + 4.0f, ( x, y ) => SandPit( Solid, wood, sand, metal, x + 2.0f, y + 2.0f, box ) ) );
		if ( s.Swing ) pieces.Add( new( "swing", 84.0f, 56.0f, ( x, y ) => Swing( Solid, metal, wood, x + 42.0f, y + 28.0f ) ) );
		if ( s.Slide ) pieces.Add( new( "slide", 102.0f, 36.0f, ( x, y ) => SlideTower( Solid, wood, metal, x + 4.0f, y + 4.0f ) ) );
		if ( s.Seesaw ) pieces.Add( new( "seesaw", 124.0f, 16.0f, ( x, y ) => Seesaw( Solid, wood, metal, x + 62.0f, y + 8.0f ) ) );

		// Rows, deepest pieces first; the seed shuffles each row and spreads the room left over.
		var rows = new List<(float Y, float Depth, List<Piece> Items, float Used)>();
		var limit = w - Margin * 2.0f;
		foreach ( var piece in pieces.OrderByDescending( p => p.Depth ) )
		{
			if ( piece.Width > limit ) continue;
			var r = rows.FindIndex( row => row.Used + Spacing + piece.Width <= limit && piece.Depth <= row.Depth );
			if ( r >= 0 )
			{
				var row = rows[r];
				row.Items.Add( piece );
				rows[r] = (row.Y, row.Depth, row.Items, row.Used + Spacing + piece.Width);
				continue;
			}
			var y = rows.Count == 0 ? front : rows[^1].Y + rows[^1].Depth + Spacing;
			if ( y + piece.Depth > d - Margin ) continue;
			rows.Add( (y, piece.Depth, new List<Piece> { piece }, piece.Width) );
		}

		foreach ( var row in rows )
		{
			var items = row.Items.OrderBy( _ => stream.Next() ).ToList();
			var slack = limit - items.Sum( p => p.Width );
			var weights = items.Select( _ => 0.5f + stream.Next() ).Append( 0.5f + stream.Next() ).ToList();
			var x = Margin;
			for ( var i = 0; i < items.Count; i++ )
			{
				x += slack * weights[i] / weights.Sum();
				items[i].Build( x, row.Y + (row.Depth - items[i].Depth) * stream.Next() );
				x += items[i].Width;
			}
		}

		return root;
	}

	/// <summary>
	/// Posts, two rails and pickets round the rectangle. The front has a gate opening in the middle.
	/// </summary>
	static void Fence( Action<string, string, string, Material, Action<MeshKit>> solid, Material wood, float w, float d )
	{
		var gate0 = w * 0.5f - GateWidth * 0.5f;
		var gate1 = w * 0.5f + GateWidth * 0.5f;

		// Each run lies along one edge: from, to, the outside line, which way is inside, along X?
		var runs = new List<(float A, float B, float Line, float In, bool AlongX)>
		{
			(0.0f, gate0, 0.0f, 1.0f, true), (gate1, w, 0.0f, 1.0f, true),
			(0.0f, w, d, -1.0f, true),
			(0.0f, d, 0.0f, 1.0f, false), (0.0f, d, w, -1.0f, false),
		};

		Vector3 At( bool alongX, float a, float across, float z ) => alongX ? new Vector3( a, across, z ) : new Vector3( across, a, z );
		void Box( MeshKit kit, bool alongX, float a0, float a1, float c0, float c1, float z0, float z1 ) =>
			kit.Box( Vector3.Min( At( alongX, a0, c0, z0 ), At( alongX, a1, c1, z1 ) ), Vector3.Max( At( alongX, a0, c0, z0 ), At( alongX, a1, c1, z1 ) ) );

		var postsAt = new HashSet<(float, float)>();
		solid( "fence_posts", "Fence Posts", "PlaygroundFence", wood, kit =>
		{
			foreach ( var (a, b, line, inward, alongX) in runs )
			{
				var bays = Math.Max( 1, (int)MathF.Ceiling( (b - a) / 48.0f ) );
				for ( var k = 0; k <= bays; k++ )
				{
					var p = a + (b - a) * k / bays;
					var c = Math.Clamp( p, 1.5f, (alongX ? w : d) - 1.5f );
					var key = alongX ? (c, line + inward * 1.5f) : (line + inward * 1.5f, c);
					if ( !postsAt.Add( (MathF.Round( key.Item1, 2 ), MathF.Round( key.Item2, 2 )) ) ) continue;
					var c0 = inward > 0 ? line : line - 3.0f;
					Box( kit, alongX, c - 1.5f, c + 1.5f, c0, c0 + 3.0f, 0.0f, FenceHeight );
				}
			}
		} );

		solid( "fence_rails", "Fence Rails", "PlaygroundFence", wood, kit =>
		{
			foreach ( var (a, b, line, inward, alongX) in runs )
			{
				var bays = Math.Max( 1, (int)MathF.Ceiling( (b - a) / 48.0f ) );
				var c0 = inward > 0 ? line : line - 1.5f;
				for ( var k = 0; k < bays; k++ )
				{
					var p0 = Math.Clamp( a + (b - a) * k / bays, 1.5f, (alongX ? w : d) - 1.5f ) + 1.5f;
					var p1 = Math.Clamp( a + (b - a) * (k + 1) / bays, 1.5f, (alongX ? w : d) - 1.5f ) - 1.5f;
					if ( p1 - p0 < 1.0f ) continue;
					foreach ( var z in new[] { 8.0f, FenceHeight - 8.0f } )
						Box( kit, alongX, p0, p1, c0, c0 + 1.5f, z, z + 2.0f );
				}
			}
		} );

		solid( "fence_pickets", "Fence Pickets", "PlaygroundFence", wood, kit =>
		{
			foreach ( var (a, b, line, inward, alongX) in runs )
			{
				var bays = Math.Max( 1, (int)MathF.Ceiling( (b - a) / 48.0f ) );
				var c0 = inward > 0 ? line + 1.5f : line - 2.5f;
				for ( var k = 0; k < bays; k++ )
				{
					var p0 = Math.Clamp( a + (b - a) * k / bays, 1.5f, (alongX ? w : d) - 1.5f ) + 1.5f;
					var p1 = Math.Clamp( a + (b - a) * (k + 1) / bays, 1.5f, (alongX ? w : d) - 1.5f ) - 1.5f;
					var n = (int)MathF.Floor( (p1 - p0 - 2.0f) / 6.0f );
					for ( var i = 0; i < n; i++ )
					{
						var p = p0 + 2.0f + (p1 - p0 - 2.0f) * (i + 0.5f) / n;
						Box( kit, alongX, p - 1.0f, p + 1.0f, c0, c0 + 1.0f, 2.0f, FenceHeight - 3.0f );
					}
				}
			}
		} );
	}

	/// <summary>
	/// A park bench facing into the playground: two end frames, three seat slats, two back slats.
	/// </summary>
	static void Bench( Action<string, string, string, Material, Action<MeshKit>> solid, Material wood, Material metal, string id, float x0, float x1, float y )
	{
		solid( $"{id}_frame", "Bench Frame", "PlaygroundBench", metal, kit =>
		{
			foreach ( var e in new[] { x0 + 4.0f, x1 - 4.0f } )
			{
				kit.Box( new Vector3( e - 1.0f, y, 0.0f ), new Vector3( e + 1.0f, y + 2.0f, 34.0f ) );
				kit.Box( new Vector3( e - 1.0f, y + 16.0f, 0.0f ), new Vector3( e + 1.0f, y + 18.0f, 15.0f ) );
				kit.Box( new Vector3( e - 1.0f, y + 2.0f, 13.0f ), new Vector3( e + 1.0f, y + 16.0f, 15.0f ) );
			}
		} );
		solid( $"{id}_slats", "Bench Slats", "PlaygroundBench", wood, kit =>
		{
			for ( var k = 0; k < 3; k++ )
				kit.Box( new Vector3( x0, y + 2.5f + k * 5.5f, 15.0f ), new Vector3( x1, y + 7.0f + k * 5.5f, 16.5f ) );
			foreach ( var z in new[] { 20.0f, 27.0f } )
				kit.Box( new Vector3( x0, y + 2.0f, z ), new Vector3( x1, y + 3.2f, z + 5.0f ) );
		} );
	}

	/// <summary>
	/// Posts on a 24 grid with bars between them at three levels.
	/// </summary>
	static void Climb( Action<string, string, string, Material, Action<MeshKit>> solid, Material metal, float x0, float y0 )
	{
		const int n = 4;
		const float step = 24.0f;
		const float top = 72.0f;
		// Match the two adjoining facets of each twelve-sided post. A rectangular
		// end either leaves a gap around the round post or intersects it.
		static void Bar( MeshKit kit, Vector3 from, Vector3 to, float z )
		{
			const float radius = 1.25f;
			const float half = 0.6f;
			var facetAngle = MathF.PI / 6.0f;
			var inset = radius - half * (1.0f - MathF.Cos( facetAngle )) / MathF.Sin( facetAngle );
			var along = (to - from).Normal;
			var across = new Vector3( -along.y, along.x, 0 );
			kit.Prism( [from + along * inset - across * half, to - along * inset - across * half, to - along * radius, to - along * inset + across * half, from + along * inset + across * half, from + along * radius], z - half, z + half );
		}

		float Node( float o, int i ) => o + 2.0f + step * i;
		solid( "climb_posts", "Climbing Frame Posts", "PlaygroundClimb", metal, kit =>
		{
			for ( var i = 0; i < n; i++ )
				for ( var j = 0; j < n; j++ )
					kit.Cylinder( new Vector3( Node( x0, i ), Node( y0, j ), 0.0f ), 1.25f, 1.25f, 0.0f, top, 12 );
		} );
		solid( "climb_bars", "Climbing Frame Bars", "PlaygroundClimb", metal, kit =>
		{
			foreach ( var z in new[]
			{
				24.0f,
				48.0f,
				top - 1.5f
			}

	 )
				for ( var i = 0; i < n; i++ )
					for ( var j = 0; j + 1 < n; j++ )
					{
						Bar( kit, new Vector3( Node( x0, j ), Node( y0, i ), 0 ), new Vector3( Node( x0, j + 1 ), Node( y0, i ), 0 ), z );
						Bar( kit, new Vector3( Node( x0, i ), Node( y0, j ), 0 ), new Vector3( Node( x0, i ), Node( y0, j + 1 ), 0 ), z );
					}
		} );
	}

	/// <summary>
	/// Boards round a bed of sand, seat boards on top, a bucket in the sand.
	/// </summary>
	static void SandPit( Action<string, string, string, Material, Action<MeshKit>> solid, Material wood, Material sand, Material metal, float x0, float y0, float size )
	{
		var x1 = x0 + size;
		var y1 = y0 + size;
		solid( "sandbox", "Sandbox", "PlaygroundSandbox", wood, kit =>
		{
			kit.Box( new Vector3( x0, y0, 0 ), new Vector3( x1, y0 + 4, 10 ) );
			kit.Box( new Vector3( x0, y1 - 4, 0 ), new Vector3( x1, y1, 10 ) );
			kit.Box( new Vector3( x0, y0 + 4, 0 ), new Vector3( x0 + 4, y1 - 4, 10 ) );
			kit.Box( new Vector3( x1 - 4, y0 + 4, 0 ), new Vector3( x1, y1 - 4, 10 ) );
		} );
		solid( "sandbox_seats", "Seat Boards", "PlaygroundSandbox", wood, kit =>
		{
			kit.Box( new Vector3( x0 - 2, y0 - 2, 10 ), new Vector3( x1 + 2, y0 + 6, 11.5f ) );
			kit.Box( new Vector3( x0 - 2, y1 - 6, 10 ), new Vector3( x1 + 2, y1 + 2, 11.5f ) );
			kit.Box( new Vector3( x0 - 2, y0 + 6, 10 ), new Vector3( x0 + 6, y1 - 6, 11.5f ) );
			kit.Box( new Vector3( x1 - 6, y0 + 6, 10 ), new Vector3( x1 + 2, y1 - 6, 11.5f ) );
		} );
		solid( "sand", "Sand", "PlaygroundSand", sand, kit => kit.Box( new Vector3( x0 + 4, y0 + 4, 0 ), new Vector3( x1 - 4, y1 - 4, 7 ) ) );
		solid( "bucket", "Bucket", "PlaygroundToy", metal, kit => kit.Lathe( new Vector3( x0 + size * 0.35f, y0 + size * 0.6f, 0 ),
			[new( 0, 7 ), new( 3, 7 ), new( 4, 13 ), new( 3.4f, 13 ), new( 2.6f, 8 ), new( 0, 8 )], 16 ) );
	}

	/// <summary>
	/// An A-frame of four splayed legs under a top beam, two seats on chains.
	/// </summary>
	static void Swing( Action<string, string, string, Material, Action<MeshKit>> solid, Material metal, Material wood, float cx, float cy )
	{
		const float beam = 84.0f;
		const float splay = 26.0f;
		const float leg = 3.0f;
		var len = MathF.Sqrt( (splay - 2.6f) * (splay - 2.6f) + beam * beam );
		var sink = leg * 0.5f * (splay - 2.6f) / len;

		solid( "swing_frame", "Swing Frame", "PlaygroundSwing", metal, kit =>
		{
			kit.Box( new Vector3( cx - 42, cy - 2, beam - 4 ), new Vector3( cx + 42, cy + 2, beam ) );
			foreach ( var x in new[] { cx - 36.0f, cx + 36.0f } )
				foreach ( var sign in new[] { -1.0f, 1.0f } )
					kit.Beam( new Vector3( x, cy + sign * splay, sink ), new Vector3( x, cy + sign * 2.6f, beam - 4 - 1.6f ), leg, leg );
		} );
		solid( "swing_chains", "Swing Chains", "PlaygroundSwing", metal, kit =>
		{
			foreach ( var x in new[] { cx - 14.0f, cx + 14.0f } )
				foreach ( var dx in new[] { -7.0f, 7.0f } )
					kit.Box( new Vector3( x + dx - 0.25f, cy - 0.25f, 19.5f ), new Vector3( x + dx + 0.25f, cy + 0.25f, beam - 4 ) );
		} );
		solid( "swing_seats", "Swing Seats", "PlaygroundSwing", wood, kit =>
		{
			foreach ( var x in new[] { cx - 14.0f, cx + 14.0f } )
				kit.Box( new Vector3( x - 9, cy - 3.5f, 18 ), new Vector3( x + 9, cy + 3.5f, 19.5f ) );
		} );
	}

	/// <summary>
	/// A deck on four posts under a roof, a ladder up one side and a slide down the other.
	/// </summary>
	static void SlideTower( Action<string, string, string, Material, Action<MeshKit>> solid, Material wood, Material metal, float px, float py )
	{
		const float deck = 48.0f;
		const float size = 28.0f;
		const float run = 52.0f;
		const float outRun = 18.0f;
		const float exit = 12.0f;
		var xa = px + size;
		var xb = xa + run;
		var xc = xb + outRun;

		solid( "slide_tower", "Slide Tower", "PlaygroundSlide", wood, kit =>
		{
			foreach ( var (x, y) in new[] { (px - 3, py - 3), (px + size, py - 3), (px - 3, py + size), (px + size, py + size) } )
				kit.Box( new Vector3( x, y, 0 ), new Vector3( x + 3, y + 3, deck + 30 ) );
			kit.Box( new Vector3( px, py, deck - 2 ), new Vector3( px + size, py + size, deck ) );
			foreach ( var y in new[] { py - 2.5f, py + size + 0.5f } )
				foreach ( var z in new[] { deck + 12.0f, deck + 26.0f } )
					kit.Box( new Vector3( px, y, z ), new Vector3( px + size, y + 2.0f, z + 2.0f ) );
		} );
		solid( "slide_roof", "Slide Roof", "PlaygroundSlide", wood, kit => kit.Box( new Vector3( px - 4, py - 4, deck + 30 ), new Vector3( px + size + 4, py + size + 4, deck + 32 ) ) );

		solid( "slide_ladder", "Slide Ladder", "PlaygroundSlide", wood, kit =>
		{
			foreach ( var y in new[] { py + 6.0f, py + 20.5f } )
				kit.Box( new Vector3( px - 2, y, 0 ), new Vector3( px, y + 1.5f, deck + 30 ) );
			for ( var z = 8.0f; z < deck - 4.0f; z += 9.0f )
				kit.Box( new Vector3( px - 1.6f, py + 7.5f, z ), new Vector3( px - 0.4f, py + 20.5f, z + 1.2f ) );
		} );

		solid( "slide_chute", "Slide", "PlaygroundSlide", metal, kit =>
		{
			kit.Profile( [new( xa, deck - 1.5f ), new( xb, exit - 1.5f ), new( xb, exit ), new( xa, deck )], py + 5.0f, py + 23.0f );
			kit.Profile( [new( xb, exit - 1.5f ), new( xc, exit - 1.5f ), new( xc, exit ), new( xb, exit )], py + 5.0f, py + 23.0f );
			foreach ( var (y0, y1) in new[] { (py + 3.5f, py + 5.0f), (py + 23.0f, py + 24.5f) } )
			{
				kit.Profile( [new( xa, deck - 1.5f ), new( xb, exit - 1.5f ), new( xb, exit + 6.0f ), new( xa, deck + 6.0f )], y0, y1 );
				kit.Profile( [new( xb, exit - 1.5f ), new( xc, exit - 1.5f ), new( xc, exit + 6.0f ), new( xb, exit + 6.0f )], y0, y1 );
			}
		} );
		solid( "slide_legs", "Slide Legs", "PlaygroundSlide", metal, kit =>
		{
			foreach ( var y in new[] { py + 6.0f, py + 20.0f } )
				kit.Box( new Vector3( xc - 4, y, 0 ), new Vector3( xc - 2, y + 2, exit - 1.5f ) );
		} );
	}

	/// <summary>
	/// A plank tilted on a stand, with handles and seat pads at both ends.
	/// </summary>
	static void Seesaw( Action<string, string, string, Material, Action<MeshKit>> solid, Material wood, Material metal, float cx, float cy )
	{
		const float pivot = 24.0f;
		const float half = 60.0f;
		var tilt = MathF.Asin( (pivot - 3.0f - 1.0f) / half );
		var dir = new Vector3( MathF.Cos( tilt ), 0, MathF.Sin( tilt ) );

		solid( "seesaw_stand", "Seesaw Stand", "PlaygroundSeesaw", metal, kit =>
		{
			foreach ( var (y0, y1) in new[] { (cy - 6.0f, cy - 4.0f), (cy + 4.0f, cy + 6.0f) } )
				kit.Profile( [new( cx - 10, 0 ), new( cx + 10, 0 ), new( cx + 1.5f, pivot - 2 ), new( cx - 1.5f, pivot - 2 )], y0, y1 );
		} );
		solid( "seesaw_plank", "Seesaw", "PlaygroundSeesaw", wood, kit =>
		{
			kit.Frame = new Transform( new Vector3( cx, cy, pivot ), Rotation.LookAt( dir, Vector3.Up ) );
			kit.Box( new Vector3( -half, -3.9f, -1.0f ), new Vector3( half, 3.9f, 1.0f ) );
			foreach ( var x in new[] { -46.0f, 46.0f } )
			{
				foreach ( var y in new[] { -3.0f, 3.0f } )
					kit.Box( new Vector3( x - 0.75f, y - 0.75f, 1.0f ), new Vector3( x + 0.75f, y + 0.75f, 12.0f ) );
				kit.Box( new Vector3( x - 0.75f, -6.0f, 12.0f ), new Vector3( x + 0.75f, 6.0f, 13.5f ) );
				var pad = MathF.Sign( x ) * 53.0f;
				kit.Box( new Vector3( pad - 5.0f, -3.5f, 1.0f ), new Vector3( pad + 5.0f, 3.5f, 3.0f ) );
			}
		} );
	}
}

/// <summary>
/// Draws a playground on a rectangle. Which pieces appear is set in the settings.
/// </summary>
[ToolCategory( "Street" )]
[Title( "Playground" ), Icon( "toys" )]
public sealed class PlaygroundEditor( PrimitiveTool tool ) : PartBuildingEditor( tool )
{
	readonly PlaygroundGenerator _generator = new();

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

	protected override IPartGenerator Generator => _generator;

	protected override GeneratorSettings Parts => Settings;

	protected override void Prepare()
	{
		Settings.Width = RectLength;
		Settings.Depth = RectDepth;
		Settings.WoodMaterial = PartTool.Pick( Settings.WoodMaterial, SurfaceMaterial );
		Settings.MetalMaterial = PartTool.Pick( Settings.MetalMaterial, TrimMaterial );
	}
}