editor/AdvancedMappingTools/Placement/StampEditors.cs

Editor tooling for the Advanced Mapping Tools package. Defines a base StampEditor that places one copy of a shape per click and merges them into a single MeshKit, plus concrete editors for columns, turned props, crate stacks, containers, gravestones, pews, crosses, tank traps, wells, chimneys and watchtowers. Each editor exposes properties and builds geometry in Stamp.

namespace AdvancedMappingTools;
/// <summary>
/// Every click sets one copy of a shape, all of them joined into one mesh.
/// </summary>
public abstract class StampEditor( PrimitiveTool tool ) : BuilderEditor( tool )
{
	protected override int MinPoints => 1;
	protected override bool ConnectPoints => false;

	protected override IEnumerable<string> Hints => ["Each click: place one", "Enter: finish"];

	[Property, Group( "Placing" ), Range( -180, 180, slider: false ), WideMode]
	public float Yaw { get; set; } = 0.0f;

	[Property, Group( "Placing" ), Title( "Random Turn" ), Range( 0, 180, slider: false ), WideMode]
	public float RandomTurn { get; set; } = 0.0f;

	[Property, Group( "Placing" ), Range( 0, 9999, slider: false ), WideMode]
	public int Seed { get; set; } = 1;

	protected abstract void Stamp( MeshKit kit, Transform frame, Random random );

	protected override void BuildMesh( MeshKit kit )
	{
		var random = new Random( Seed );

		foreach ( var point in Points )
		{
			var yaw = Yaw + ((float)random.NextDouble() - 0.5f) * RandomTurn * 2.0f;
			var frame = new Transform( point.Position, Rotation.FromYaw( yaw ) );

			kit.Material = SurfaceMaterial;
			kit.Frame = frame;
			Stamp( kit, frame, random );
		}
	}
}

/// <summary>
/// Columns and pillars, each with a base and a capital.
/// </summary>
[ToolCategory( "Columns & Towers" )]
[Title( "Column" ), Icon( "view_column" )]
public sealed partial class ColumnEditor( PrimitiveTool tool ) : GroundAwareStampEditor( tool )
{
	public enum ColumnStyle
	{
		Round,
		Square,
		Tapered
	}

	[Property, Group( "Column" ), WideMode]
	public ColumnStyle Style { get; set; } = ColumnStyle.Round;

	[Property, Group( "Column" ), Range( 1, 512, slider: false ), WideMode]
	public float Radius { get; set; } = 12.0f;

	[Property, Group( "Column" ), Range( 8, 4096, slider: false ), WideMode]
	public float Height { get; set; } = 160.0f;

	[Property, Group( "Column" ), Title( "Base Height" ), Range( 0, 128, slider: false ), WideMode]
	public float BaseHeight { get; set; } = 12.0f;

	[Property, Group( "Column" ), Title( "Capital Height" ), Range( 0, 128, slider: false ), WideMode]
	public float CapitalHeight { get; set; } = 12.0f;

	[Property, Group( "Column" ), Range( 4, 48, slider: false ), WideMode]
	public int Sides { get; set; } = 16;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var height = Height;
		if ( FitToGround )
		{
			var scene = SceneEditorSession.Active?.Scene ?? Game.ActiveScene;
			var read = GroundRead ?? SurfaceSolver.Begin( scene );
			var top = frame.PointToWorld( Vector3.Up * Height );
			var sample = read.Probe( top, Vector3.Down, Options() );
			if ( read.AcceptBelow( sample, sample.Position.z, top.z ) )
			{
				frame.Position = sample.Position;
				height = top.z - sample.Position.z;
				kit.Frame = frame;
			}

			SurfaceDebug.Record( "Column", read );
		}

		var low = MathF.Min( BaseHeight, height * 0.3f );
		var high = height - MathF.Min( CapitalHeight, height * 0.3f );
		var r = Radius;
		var wide = r * 1.4f;
		if ( Style == ColumnStyle.Square )
		{
			kit.Box( new Vector3( -r, -r, low ), new Vector3( r, r, high ) );
		}
		else
		{
			kit.Cylinder( Vector3.Zero, r, Style == ColumnStyle.Tapered ? r * 0.75f : r, low, high, Math.Clamp( Sides, 4, 48 ) );
		}

		kit.Material = TrimMaterial;
		if ( low > 0.0f )
			kit.Box( new Vector3( -wide, -wide, 0.0f ), new Vector3( wide, wide, low ) );
		if ( CapitalHeight > 0.0f )
			kit.Box( new Vector3( -wide, -wide, high ), new Vector3( wide, wide, height ) );
	}
}

/// <summary>
/// Turned props made from one outline: barrels, posts, lamps and urns.
/// </summary>
[ToolCategory( "Props" )]
[Title( "Turned Prop" ), Icon( "local_drink" )]
public sealed class TurnedPropEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Trim" };

	public enum PropShape
	{
		Barrel,
		Bollard,
		Urn,
		[Title( "Lamp Post" )] LampPost,
		[Title( "Oil Drum" )] OilDrum,
		Silo,
		Pinnacle,
		Obelisk,
		Baluster,
		Finial
	}

	[Property, Group( "Turned Prop" ), WideMode]
	public PropShape Shape { get; set; } = PropShape.Barrel;

	[Property, Group( "Turned Prop" ), Range( 0.1f, 32, slider: false ), WideMode]
	public float Scale { get; set; } = 1.0f;

	[Property, Group( "Turned Prop" ), Range( 4, 48, slider: false ), WideMode]
	public int Sides { get; set; } = 12;

	static readonly Dictionary<PropShape, Vector2[]> Outlines = new()
	{
		[PropShape.Barrel] = [new( 0, 0 ), new( 12, 0 ), new( 15, 12 ), new( 16, 22 ), new( 15, 32 ), new( 12, 44 ), new( 0, 44 )],
		[PropShape.Bollard] = [new( 0, 0 ), new( 5, 0 ), new( 4, 28 ), new( 5, 32 ), new( 3, 36 ), new( 0, 36 )],
		[PropShape.Urn] = [new( 0, 0 ), new( 8, 0 ), new( 3, 4 ), new( 12, 18 ), new( 13, 26 ), new( 7, 34 ), new( 9, 38 ), new( 0, 38 )],
		[PropShape.LampPost] = [new( 0, 0 ), new( 6, 0 ), new( 2, 6 ), new( 1.5f, 120 ), new( 5, 124 ), new( 7, 138 ), new( 3, 146 ), new( 0, 148 )],
		[PropShape.OilDrum] = [new( 0, 0 ), new( 12, 0 ), new( 12.5f, 12 ), new( 12, 12.5f ), new( 12.5f, 24 ), new( 12, 24.5f ), new( 12, 36 ), new( 0, 36 )],
		[PropShape.Silo] = [new( 0, 0 ), new( 64, 0 ), new( 64, 256 ), new( 48, 296 ), new( 0, 312 )],
		[PropShape.Pinnacle] = [new( 0, 0 ), new( 10, 0 ), new( 10, 10 ), new( 7, 12 ), new( 1.5f, 52 ), new( 3, 54 ), new( 4, 57 ), new( 3, 60 ), new( 0, 62 )],
		[PropShape.Obelisk] = [new( 0, 0 ), new( 16, 0 ), new( 16, 12 ), new( 9, 16 ), new( 4, 110 ), new( 0, 120 )],
		[PropShape.Baluster] = [new( 0, 0 ), new( 4, 0 ), new( 4, 3 ), new( 2, 6 ), new( 4.5f, 14 ), new( 1.5f, 28 ), new( 3.5f, 30 ), new( 3.5f, 33 ), new( 0, 33 )],
		[PropShape.Finial] = [new( 0, 0 ), new( 4, 0 ), new( 2, 4 ), new( 2, 8 ), new( 5, 12 ), new( 6, 16 ), new( 5, 20 ), new( 2, 24 ), new( 0, 30 )]
	};

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var scale = MathF.Max( Scale, 0.1f );
		var outline = Outlines[Shape].Select( p => p * scale ).ToArray();

		kit.Lathe( Vector3.Zero, outline, Math.Clamp( Sides, 4, 48 ) );
	}
}

/// <summary>
/// Stacked wooden crates with framed edges.
/// </summary>
[ToolCategory( "Props" )]
[Title( "Crate Stack" ), Icon( "inventory_2" )]
public sealed class CrateEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	[Property, Group( "Crates" ), Range( 4, 256, slider: false ), WideMode]
	public float Size { get; set; } = 32.0f;

	[Property, Group( "Crates" ), Title( "Base Rows" ), Range( 1, 8, slider: false ), WideMode]
	public int BaseRows { get; set; } = 2;

	[Property, Group( "Crates" ), Range( 1, 8, slider: false ), WideMode]
	public int Levels { get; set; } = 2;

	[Property, Group( "Crates" ), Title( "Framed Edges" ), WideMode]
	public bool FramedEdges { get; set; } = true;

	void Crate( MeshKit kit, Vector3 at, float turn, Material surface, Material trim )
	{
		var half = Size * 0.5f;
		var frame = kit.Frame;
		var e = Size * 0.06f;

		kit.Frame = frame.ToWorld( new Transform( at, Rotation.FromYaw( turn ) ) );
		kit.Material = surface;
		kit.Box( new Vector3( -half + e, -half + e, e ), new Vector3( half - e, half - e, Size - e ) );

		if ( FramedEdges )
		{
			kit.Material = trim;

			foreach ( var z in new[] { 0.0f, Size - e * 2.0f } )
			{
				kit.Box( new Vector3( -half, -half, z ), new Vector3( half, -half + e * 2.0f, z + e * 2.0f ) );
				kit.Box( new Vector3( -half, half - e * 2.0f, z ), new Vector3( half, half, z + e * 2.0f ) );
				kit.Box( new Vector3( -half, -half + e * 2.0f, z ), new Vector3( -half + e * 2.0f, half - e * 2.0f, z + e * 2.0f ) );
				kit.Box( new Vector3( half - e * 2.0f, -half + e * 2.0f, z ), new Vector3( half, half - e * 2.0f, z + e * 2.0f ) );
			}
		}

		kit.Frame = frame;
	}

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		for ( var level = 0; level < Levels; level++ )
		{
			var rows = BaseRows - level;
			if ( rows <= 0 ) break;

			var shift = (rows - 1) * Size * 0.5f;

			for ( var x = 0; x < rows; x++ )
			{
				for ( var y = 0; y < rows; y++ )
				{
					var at = new Vector3( x * Size - shift, y * Size - shift, level * Size );
					Crate( kit, at, ((float)random.NextDouble() - 0.5f) * 8.0f, SurfaceMaterial, TrimMaterial );
				}
			}
		}
	}
}

/// <summary>
/// Ribbed shipping containers.
/// </summary>
[ToolCategory( "Props" )]
[Title( "Container" ), Icon( "local_shipping" )]
public sealed class ContainerEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	[Property, Group( "Container" ), Range( 32, 1024, slider: false ), WideMode]
	public float Length { get; set; } = 240.0f;

	[Property, Group( "Container" ), Range( 16, 256, slider: false ), WideMode]
	public float Width { get; set; } = 96.0f;

	[Property, Group( "Container" ), Range( 16, 256, slider: false ), WideMode]
	public float Height { get; set; } = 104.0f;

	[Property, Group( "Container" ), Title( "Rib Spacing" ), Range( 4, 64, slider: false ), WideMode]
	public float RibSpacing { get; set; } = 16.0f;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var l = Length * 0.5f;
		var w = Width * 0.5f;

		kit.Box( new Vector3( -l, -w, 4.0f ), new Vector3( l, w, Height - 4.0f ) );

		kit.Material = TrimMaterial;

		for ( var x = -l + 4.0f; x < l - 4.0f; x += MathF.Max( RibSpacing, 4.0f ) )
		{
			kit.Box( new Vector3( x, -w - 1.0f, 4.0f ), new Vector3( x + RibSpacing * 0.4f, -w, Height - 4.0f ) );
			kit.Box( new Vector3( x, w, 4.0f ), new Vector3( x + RibSpacing * 0.4f, w + 1.0f, Height - 4.0f ) );
		}

		kit.Box( new Vector3( -l - 1.0f, -w - 1.0f, 0.0f ), new Vector3( l + 1.0f, w + 1.0f, 4.0f ) );
		kit.Box( new Vector3( -l - 1.0f, -w - 1.0f, Height - 4.0f ), new Vector3( l + 1.0f, w + 1.0f, Height ) );

		foreach ( var (x, y) in new[] { (-l - 1.0f, -w - 1.0f), (l - 3.0f, -w - 1.0f), (-l - 1.0f, w - 3.0f), (l - 3.0f, w - 3.0f) } )
			kit.Box( new Vector3( x, y, 4.0f ), new Vector3( x + 4.0f, y + 4.0f, Height - 4.0f ) );
	}
}

/// <summary>
/// Gravestones in a few shapes, tilted a little.
/// </summary>
[ToolCategory( "Props" )]
[Title( "Gravestone" ), Icon( "bookmark" )]
public sealed class GravestoneEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	public enum StoneShape
	{
		Rounded,
		Pointed,
		Slab,
		Cross
	}

	[Property, Group( "Gravestone" ), WideMode]
	public StoneShape Shape { get; set; } = StoneShape.Rounded;

	[Property, Group( "Gravestone" ), Range( 4, 128, slider: false ), WideMode]
	public float Width { get; set; } = 20.0f;

	[Property, Group( "Gravestone" ), Range( 8, 256, slider: false ), WideMode]
	public float Height { get; set; } = 36.0f;

	[Property, Group( "Gravestone" ), Range( 1, 32, slider: false ), WideMode]
	public float Thickness { get; set; } = 4.0f;

	[Property, Group( "Gravestone" ), Title( "Max Tilt" ), Range( 0, 30, slider: false ), WideMode]
	public float MaxTilt { get; set; } = 6.0f;

	[Property, Group( "Gravestone" ), Title( "Grave Mound" ), WideMode]
	public bool GraveMound { get; set; } = false;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var half = Width * 0.5f;
		var depth = Thickness * 0.5f;
		var pitch = ((float)random.NextDouble() * 2.0f - 1.0f) * MaxTilt;
		var roll = ((float)random.NextDouble() * 2.0f - 1.0f) * MaxTilt;

		kit.Frame = frame.ToWorld( new Transform( Vector3.Zero, Rotation.From( pitch, 0.0f, roll ) ) );

		switch ( Shape )
		{
			case StoneShape.Rounded:
			case StoneShape.Pointed:
				var style = Shape == StoneShape.Rounded ? ArchStyle.Round : ArchStyle.Pointed;
				var rise = Shape == StoneShape.Rounded ? half : Width * 0.866f;
				var spring = MathF.Max( Height - rise, 1.0f );
				var curve = MeshKit.ArchCurve( -half, half, spring, rise, style, 10 );
				kit.Profile( [new( -half, 0 ), new( half, 0 ), .. curve.Reverse()], -depth, depth );
				break;

			case StoneShape.Slab:
				kit.Box( new Vector3( -half, -depth, 0.0f ), new Vector3( half, depth, Height ) );
				break;

			case StoneShape.Cross:
				var bar = Width * 0.15f;
				kit.Box( new Vector3( -bar, -depth, 0.0f ), new Vector3( bar, depth, Height ) );
				kit.Box( new Vector3( -half, -depth, Height * 0.6f ), new Vector3( half, depth, Height * 0.6f + bar * 2.0f ) );
				break;
		}

		kit.Frame = frame;

		if ( !GraveMound ) return;

		kit.Material = TrimMaterial;
		kit.Hex(
			new( -half, depth, 0 ), new( half, depth, 0 ), new( half, depth + 72.0f, 0 ), new( -half, depth + 72.0f, 0 ),
			new( -half * 0.7f, depth + 4.0f, 8 ), new( half * 0.7f, depth + 4.0f, 8 ), new( half * 0.7f, depth + 68.0f, 8 ), new( -half * 0.7f, depth + 68.0f, 8 ) );
	}
}

/// <summary>
/// Church pews and benches, in rows along the local X axis.
/// </summary>
[ToolCategory( "Props" )]
[Title( "Pews" ), Icon( "chair" )]
public sealed class PewEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Trim" };

	[Property, Group( "Pews" ), Range( 1, 64, slider: false ), WideMode]
	public int Rows { get; set; } = 6;

	[Property, Group( "Pews" ), Title( "Row Spacing" ), Range( 16, 256, slider: false ), WideMode]
	public float RowSpacing { get; set; } = 48.0f;

	[Property, Group( "Pews" ), Range( 16, 512, slider: false ), WideMode]
	public float Length { get; set; } = 112.0f;

	[Property, Group( "Pews" ), Title( "Seat Height" ), Range( 8, 64, slider: false ), WideMode]
	public float SeatHeight { get; set; } = 18.0f;

	[Property, Group( "Pews" ), Title( "Back Height" ), Range( 8, 96, slider: false ), WideMode]
	public float BackHeight { get; set; } = 36.0f;

	[Property, Group( "Pews" ), Title( "Center Aisle" ), Range( 0, 256, slider: false ), WideMode, Description( "Sets a second block of pews on the other side of an aisle this wide." )]
	public float CenterAisle { get; set; } = 0.0f;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		for ( var row = 0; row < Rows; row++ )
		{
			var x = row * RowSpacing;

			Pew( kit, x, 0.0f );
			if ( CenterAisle > 0.0f ) Pew( kit, x, -Length - CenterAisle );
		}
	}

	void Pew( MeshKit kit, float x, float y0 )
	{
		var y1 = y0 + Length;
		var seatDepth = 16.0f;

		kit.Box( new Vector3( x, y0, SeatHeight - 2.0f ), new Vector3( x + seatDepth, y1, SeatHeight ) );
		kit.Box( new Vector3( x - 2.0f, y0, SeatHeight ), new Vector3( x, y1, BackHeight ) );

		foreach ( var y in new[] { y0, y1 - 2.0f } )
			kit.Box( new Vector3( x - 2.0f, y, 0.0f ), new Vector3( x + seatDepth, y + 2.0f, BackHeight - 4.0f ) );
	}
}

/// <summary>
/// Standing crosses, for graveyards and roofs.
/// </summary>
[ToolCategory( "Props" )]
[Title( "Cross" ), Icon( "add" )]
public sealed class CrossEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	[Property, Group( "Cross" ), Range( 4, 1024, slider: false ), WideMode]
	public float Height { get; set; } = 72.0f;

	[Property, Group( "Cross" ), Range( 0.5f, 64, slider: false ), WideMode]
	public float Thickness { get; set; } = 3.0f;

	[Property, Group( "Cross" ), Title( "Stepped Base" ), WideMode]
	public bool SteppedBase { get; set; } = true;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var t = Thickness;
		var arm = Height * 0.3f;
		var z = SteppedBase ? t * 4.0f : 0.0f;

		kit.Box( new Vector3( -t, -t, z ), new Vector3( t, t, z + Height ) );
		kit.Box( new Vector3( -t, -arm, z + Height * 0.65f ), new Vector3( t, arm, z + Height * 0.65f + t * 2.0f ) );

		if ( !SteppedBase ) return;

		kit.Material = TrimMaterial;
		kit.Box( new Vector3( -t * 6.0f, -t * 6.0f, 0.0f ), new Vector3( t * 6.0f, t * 6.0f, t * 2.0f ) );
		kit.Box( new Vector3( -t * 4.0f, -t * 4.0f, t * 2.0f ), new Vector3( t * 4.0f, t * 4.0f, t * 4.0f ) );
	}
}

/// <summary>
/// Steel anti-tank obstacles made of three crossed beams.
/// </summary>
[ToolCategory( "Defense" )]
[Title( "Tank Trap" ), Icon( "close" )]
public sealed class TankTrapEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Trim" };

	[Property, Group( "Tank Trap" ), Range( 16, 256, slider: false ), WideMode]
	public float Size { get; set; } = 64.0f;

	[Property, Group( "Tank Trap" ), Title( "Beam Size" ), Range( 1, 32, slider: false ), WideMode]
	public float BeamSize { get; set; } = 5.0f;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var r = Size * 0.5f;
		var z = r * 0.8f;

		kit.Beam( new Vector3( -r, -r, 0.0f ), new Vector3( r, r, z * 2.0f ), BeamSize, BeamSize );
		kit.Beam( new Vector3( r, -r, 0.0f ), new Vector3( -r, r, z * 2.0f ), BeamSize, BeamSize );
		kit.Beam( new Vector3( 0.0f, -r * 1.2f, z * 2.0f ), new Vector3( 0.0f, r * 1.2f, 0.0f ), BeamSize, BeamSize );
	}
}

/// <summary>
/// A stone well with a little roof.
/// </summary>
[ToolCategory( "Columns & Towers" )]
[Title( "Well" ), Icon( "water_drop" )]
public sealed class WellEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	[Property, Group( "Well" ), Range( 8, 128, slider: false ), WideMode]
	public float Radius { get; set; } = 28.0f;

	[Property, Group( "Well" ), Title( "Rim Height" ), Range( 8, 96, slider: false ), WideMode]
	public float RimHeight { get; set; } = 32.0f;

	[Property, Group( "Well" ), Title( "Roof" ), WideMode]
	public bool HasRoof { get; set; } = true;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		kit.Ring( Vector3.Zero, Radius, Radius - 6.0f, 0.0f, RimHeight, 16 );

		if ( !HasRoof ) return;

		var height = RimHeight + 56.0f;
		var edge = Radius + 8.0f;

		kit.Material = TrimMaterial;
		kit.Box( new Vector3( -2.0f, -Radius - 2.0f, 0.0f ), new Vector3( 2.0f, -Radius + 2.0f, height ) );
		kit.Box( new Vector3( -2.0f, Radius - 2.0f, 0.0f ), new Vector3( 2.0f, Radius + 2.0f, height ) );

		var top = frame.ToWorld( new Transform( new Vector3( 0.0f, 0.0f, height ) ) );
		kit.Frame = top;
		RoofEditor.BuildRoof( kit, top, -edge, edge, -edge, edge, RoofShape.Gable, 24.0f, 2.0f, SurfaceMaterial, TrimMaterial );
		kit.Frame = frame;
	}
}

/// <summary>
/// A brick chimney with a cap and pots.
/// </summary>
[ToolCategory( "Roofs" )]
[Title( "Chimney" ), Icon( "fireplace" )]
public sealed class ChimneyEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	[Property, Group( "Chimney" ), Range( 16, 1024, slider: false ), WideMode]
	public float Height { get; set; } = 96.0f;

	[Property, Group( "Chimney" ), Range( 8, 256, slider: false ), WideMode]
	public float Width { get; set; } = 32.0f;

	[Property, Group( "Chimney" ), Range( 0, 6, slider: false ), WideMode]
	public int Pots { get; set; } = 2;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var half = Width * 0.5f;

		kit.Box( new Vector3( -half, -half, 0.0f ), new Vector3( half, half, Height ) );

		kit.Material = TrimMaterial;
		kit.Box( new Vector3( -half - 3.0f, -half - 3.0f, Height ), new Vector3( half + 3.0f, half + 3.0f, Height + 4.0f ) );

		for ( var i = 0; i < Pots; i++ )
		{
			var y = Width * (i + 0.5f) / Pots - half;
			kit.Ring( new Vector3( 0.0f, y, 0.0f ), 4.0f, 2.5f, Height + 4.0f, Height + 18.0f, 8 );
		}
	}
}

/// <summary>
/// A wooden guard tower on four legs with a platform, railing and shed roof.
/// </summary>
[ToolCategory( "Columns & Towers" )]
[Title( "Watchtower" ), Icon( "cell_tower" )]
public sealed class WatchtowerEditor( PrimitiveTool tool ) : StampEditor( tool )
{
	[Property, Group( "Watchtower" ), Range( 64, 2048, slider: false ), WideMode]
	public float Height { get; set; } = 256.0f;

	[Property, Group( "Watchtower" ), Title( "Base Size" ), Range( 32, 512, slider: false ), WideMode]
	public float BaseSize { get; set; } = 128.0f;

	[Property, Group( "Watchtower" ), Title( "Platform Size" ), Range( 32, 512, slider: false ), WideMode]
	public float PlatformSize { get; set; } = 96.0f;

	[Property, Group( "Watchtower" ), Range( 0, 16, slider: false ), WideMode]
	public int Braces { get; set; } = 3;

	[Property, Group( "Watchtower" ), Title( "Has Roof" ), WideMode]
	public bool HasRoof { get; set; } = true;

	protected override void Stamp( MeshKit kit, Transform frame, Random random )
	{
		var low = BaseSize * 0.5f;
		var high = PlatformSize * 0.5f - 4.0f;
		var signs = new[] { (-1f, -1f), (1f, -1f), (1f, 1f), (-1f, 1f) };

		Vector3 Leg( (float X, float Y) s, float t ) => new(
			s.X * (low + (high - low) * t ), s.Y * (low + (high - low) * t ), Height * t );

		foreach ( var s in signs )
			kit.Beam( Leg( s, 0.0f ), Leg( s, 1.0f ), 6.0f, 6.0f );

		kit.Material = TrimMaterial;

		for ( var b = 0; b < Braces; b++ )
		{
			var t0 = b / (float)Braces;
			var t1 = (b + 1) / (float)Braces;

			for ( var i = 0; i < 4; i++ )
			{
				var a = signs[i];
				var c = signs[(i + 1) % 4];

				kit.Beam( Leg( a, t0 ), Leg( c, t1 ), 3.0f, 3.0f );
				kit.Beam( Leg( c, t0 ), Leg( a, t1 ), 3.0f, 3.0f );
				kit.Beam( Leg( a, t1 ), Leg( c, t1 ), 3.0f, 3.0f );
			}
		}

		var size = PlatformSize * 0.5f;

		kit.Material = SurfaceMaterial;
		kit.Box( new Vector3( -size, -size, Height ), new Vector3( size, size, Height + 4.0f ) );

		kit.Material = TrimMaterial;

		foreach ( var (x0, y0, x1, y1) in new[] { (-size, -size, size, -size + 2.0f), (-size, size - 2.0f, size, size), (-size, -size, -size + 2.0f, size), (size - 2.0f, -size, size, size) } )
			kit.Box( new Vector3( x0, y0, Height + 4.0f ), new Vector3( x1, y1, Height + 40.0f ) );

		if ( !HasRoof ) return;

		foreach ( var s in signs )
			kit.Beam( new Vector3( s.Item1 * (size - 2.0f), s.Item2 * (size - 2.0f), Height + 4.0f ), new Vector3( s.Item1 * (size - 2.0f), s.Item2 * (size - 2.0f), Height + 96.0f ), 3.0f, 3.0f );

		var top = frame.ToWorld( new Transform( Vector3.Up * (Height + 96.0f) ) );
		kit.Frame = top;
		RoofEditor.BuildRoof( kit, top, -size - 8.0f, size + 8.0f, -size - 8.0f, size + 8.0f, RoofShape.Hip, 24.0f, 2.0f, SurfaceMaterial, TrimMaterial );
		kit.Frame = frame;
	}
}