editor/AdvancedMappingTools/Architecture/BalconyExpansion.cs

Editor-side partial class that extends the balcony generator with support columns, floor covering, and an optional corner balcony return. Adds parts to an ArchitectureStudioPlan using the generator kit.

namespace AdvancedMappingTools;
public enum BalconyFloorFinish
{
	Plain,
	Boards,
	Tiles
}

public enum BalconyCorner
{
	Left,
	Right
}

public sealed partial class BalconyStudioSettings
{
	[Property, Group( "Railing" ), WideMode]
	public bool SeparateRailingStyles { get; set; }

	[Property, Group( "Railing" ), WideMode]
	public StudioBalconyStyle FrontStyle { get; set; } = StudioBalconyStyle.Glass;

	[Property, Group( "Railing" ), WideMode]
	public StudioBalconyStyle LeftStyle { get; set; } = StudioBalconyStyle.Bars;

	[Property, Group( "Railing" ), WideMode]
	public StudioBalconyStyle RightStyle { get; set; } = StudioBalconyStyle.Bars;

	[Property, Group( "Corner Balcony" ), WideMode]
	public bool CornerBalcony { get; set; }

	[Property, Group( "Corner Balcony" ), WideMode]
	public BalconyCorner CornerSide { get; set; }

	[Property, Group( "Corner Balcony" ), Range( 16, 1024, slider: false ), WideMode]
	public float ReturnLength { get; set; } = 128;

	[Property, Group( "Supports" ), WideMode]
	public bool SupportColumns { get; set; }

	[Property, Group( "Supports" ), Range( 4, 4096, slider: false ), WideMode]
	public float ColumnHeight { get; set; } = 128;

	[Property, Group( "Supports" ), Range( 1, 32, slider: false ), WideMode]
	public float ColumnWidth { get; set; } = 6;

	[Property, Group( "Floor Finish" ), WideMode]
	public bool FloorCovering { get; set; }

	[Property, Group( "Floor Finish" ), WideMode]
	public BalconyFloorFinish FloorFinish { get; set; } = BalconyFloorFinish.Boards;

	[Property, Group( "Floor Finish" ), Range( 2, 128, slider: false ), WideMode]
	public float FloorModule { get; set; } = 12;

	[Property, Group( "Floor Finish" ), Range( .25f, 8, slider: false ), WideMode]
	public float FloorCoverThickness { get; set; } = 1;

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

public sealed partial class BalconyStudioGenerator
{
	void AddExpansion( ArchitectureStudioPlan p, BalconyStudioSettings s, float w, float d, float t, GenContext context )
	{
		if ( s.SupportColumns )
		{
			var a = MathF.Min( Math.Clamp( s.ColumnWidth, 1, 32 ), MathF.Min( w * .1f, d * .2f ) );
			foreach ( var x in new[]
			{
				0f,
				w - a
			}

 )
			{
				var fitted = false;
				if ( s.FitColumnsToGround && context.Surface is { } surface )
				{
					var top = new Vector3( x + a * .5f, d - a * .5f, s.Elevation );
					var sample = surface.Ground( top, s.ProbeOptions() );
					if ( surface.Read.AcceptBelow( sample, sample.Position.z, surface.World.PointToWorld( top ).z ) )
					{
						var bottom = surface.World.PointToLocal( sample.Position ) - Vector3.Up * s.Elevation;
						var localTop = top - Vector3.Up * s.Elevation;
						p.Add( "column_" + x, "Ground-Fitted Support", p.Detail, kit => kit.Beam( bottom, localTop, a, a ) );
						fitted = true;
					}
				}

				if ( !fitted )
					p.Box( "column_" + x, "Support Column", p.Detail, new( x, d - a, -Math.Clamp( s.ColumnHeight, 4, 4096 ) ), new( x + a, d, 0 ) );
			}

			if ( s.FitColumnsToGround )
			{
				if ( context.Surface != null )
					SurfaceDebug.Record( "Balcony Studio", context.Surface.Read );
				else
					SurfaceDebug.Note( "Balcony Studio", "No scene context; original support dimensions retained." );
			}
		}

		if ( s.FloorCovering )
		{
			var edge = MathF.Min( Math.Clamp( s.FrameWidth, 1, 8 ), MathF.Min( MathF.Min( w, d ) * .2f, Math.Clamp( s.RailingHeight, 12, 128 ) * .25f ) );
			var left = s.PrivacyLeft ? Math.Clamp( s.PrivacyThickness, .25f, 8 ) : s.Left ? edge : 0;
			var right = s.PrivacyRight ? Math.Clamp( s.PrivacyThickness, .25f, 8 ) : s.Right ? edge : 0;
			var front = s.Front ? edge : 0;
			if ( s.Canopy && s.CanopyPosts )
			{
				var post = MathF.Min( Math.Clamp( s.CanopyPostWidth, 2, 12 ), MathF.Min( w, d ) * .2f );
				left = MathF.Max( left, post );
				right = MathF.Max( right, post );
				front = MathF.Max( front, post );
			}

			var coverW = MathF.Max( w - left - right, 1 );
			var coverD = MathF.Max( d - front, 1 );
			var size = Math.Clamp( s.FloorModule, 2, 128 );
			var nx = Math.Clamp( (int)MathF.Ceiling( coverW / size ), 1, 128 );
			var ny = s.FloorFinish == BalconyFloorFinish.Tiles ? Math.Clamp( (int)MathF.Ceiling( coverD / size ), 1, 128 ) : 1;
			if ( s.FloorFinish == BalconyFloorFinish.Plain )
				nx = 1;
			var h = Math.Clamp( s.FloorCoverThickness, .25f, 8 );
			var m = context.Material( s.FloorMaterial ?? s.FinishMaterial ?? s.StructureMaterial );
			p.Add( "floor_finish", "Floor Covering", m, kit =>
			{
				for ( var x = 0; x < nx; x++ )
					for ( var y = 0; y < ny; y++ )
					{
						var gap = s.FloorFinish == BalconyFloorFinish.Plain ? 0 : MathF.Min( .25f, MathF.Min( coverW / nx, coverD / ny ) * .05f );
						kit.BeginPart();
						kit.Box( new( left + x * coverW / nx + gap, y * coverD / ny + gap, t ), new( left + (x + 1) * coverW / nx - gap, (y + 1) * coverD / ny - gap, t + h ) );
					}
			} );
		}

		if ( s.CornerBalcony )
		{
			var copy = (BalconyStudioSettings)GeneratorRegistry.Load( this, GeneratorRegistry.Save( s ) );
			copy.CornerBalcony = false;
			copy.RepeatFloors = false;
			copy.Width = MathF.Min( d, w * .5f );
			copy.Depth = Math.Clamp( s.ReturnLength, 16, 1024 );
			copy.Brackets = false;
			copy.SupportColumns = false;
			copy.PrivacyLeft = false;
			copy.PrivacyRight = false;
			PartPlan Prefix( PartPlan part )
			{
				var result = new PartPlan
				{
					Id = "corner/" + part.Id,
					Name = part.Name,
					Kind = part.Kind,
					Category = part.Category,
					Seed = part.Seed,
					UsesSeed = part.UsesSeed,
					Local = part.Local,
					Mesh = part.Mesh,
					Dimensions = part.Dimensions,
					GeometryHash = part.GeometryHash,
					MaterialHash = part.MaterialHash,
					Smoothing = part.Smoothing,
					Tint = part.Tint
				};
				foreach ( var child in part.Children )
					result.Add( Prefix( child ) );
				return result;
			}

			var arm = Prefix( GenerateSingle( copy, context ) );
			arm.Local = new Transform( new Vector3( s.CornerSide == BalconyCorner.Left ? 0 : w - copy.Width, d, 0 ), Rotation.Identity );
			p.Root.Add( arm );
		}
	}
}