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