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