Editor-side helper for the Advanced Mapping Tools architecture generator. It holds the setting classes for facade fins and repeated cornices, balcony canopies and hollow roof shells, the Hide* methods that hide dependent properties in the inspector, and the geometry builders that emit fin, cornice, canopy post and hollow roof shell parts into an ArchitectureStudioPlan.
namespace AdvancedMappingTools;
public sealed partial class FacadeWallStudioSettings
{
[Property, Group( "Facade Fins" ), WideMode]
public bool Fins { get; set; }
[Property, Group( "Facade Fins" ), Range( 8, 128, slider: false ), WideMode]
public float FinSpacing { get; set; } = 32;
[Property, Group( "Facade Fins" ), Range( 1, 32, slider: false ), WideMode]
public float FinProjection { get; set; } = 12;
[Property, Group( "Facade Fins" ), Range( .5f, 8, slider: false ), WideMode]
public float FinWidth { get; set; } = 2;
[Property, Group( "Repeated Cornices" ), WideMode]
public bool RepeatedBands { get; set; }
[Property, Group( "Repeated Cornices" ), Range( 16, 256, slider: false ), WideMode]
public float CorniceSpacing { get; set; } = 48;
[Property, Group( "Repeated Cornices" ), Range( 1, 16, slider: false ), WideMode]
public float CorniceHeight { get; set; } = 4;
[Property, Group( "Repeated Cornices" ), Range( 1, 24, slider: false ), WideMode]
public float CorniceProjection { get; set; } = 6;
}
public sealed partial class BalconyStudioSettings
{
[Property, Group( "Canopy" ), WideMode]
public bool Canopy { get; set; }
[Property, Group( "Canopy" ), Range( 64, 256, slider: false ), WideMode]
public float CanopyHeight { get; set; } = 104;
[Property, Group( "Canopy" ), Range( 1, 16, slider: false ), WideMode]
public float CanopyThickness { get; set; } = 4;
[Property, Group( "Canopy" ), Range( 0, 32, slider: false ), WideMode]
public float CanopyOverhang { get; set; } = 4;
[Property, Group( "Canopy" ), WideMode]
public bool CanopyPosts { get; set; } = true;
[Property, Group( "Canopy" ), Range( 2, 12, slider: false ), WideMode]
public float CanopyPostWidth { get; set; } = 4;
}
public sealed partial class RoofStudioSettings
{
[Property, Group( "Roof Interior" ), Title( "Hollow Roof" ), WideMode, Description( "Pitched roof panels instead of a solid roof mass. Fits the drawn rectangular footprint; no automatic building scan." )]
public bool Hollow { get; set; }
[Property, Group( "Roof Interior" ), Title( "Bottom / Ceiling Closure" ), WideMode]
public bool BottomClosure { get; set; }
[Property, Group( "Roof Interior" ), Range( .5f, 32, slider: false ), WideMode]
public float BottomThickness { get; set; } = 4;
[Property, Group( "Roof Interior" ), Title( "Close Gable Ends" ), WideMode]
public bool GableEnds { get; set; } = true;
}
internal static class StudioExtras
{
internal static void HideFacade( HashSet<string> h, FacadeWallStudioSettings s )
{
if ( !s.Fins )
h.UnionWith( [nameof( s.FinSpacing ), nameof( s.FinProjection ), nameof( s.FinWidth )] );
if ( !s.RepeatedBands )
h.UnionWith( [nameof( s.CorniceSpacing ), nameof( s.CorniceHeight ), nameof( s.CorniceProjection )] );
}
internal static void HideBalcony( HashSet<string> h, BalconyStudioSettings s )
{
if ( !s.RepeatFloors )
h.UnionWith( [nameof( s.FloorCount ), nameof( s.FloorSpacing ), nameof( s.CanopyTopOnly )] );
if ( !s.Canopy )
h.UnionWith( [nameof( s.CanopyTopOnly ), nameof( s.CanopyMaterial ), nameof( s.CanopyPostMaterial ), nameof( s.CanopyHeight ), nameof( s.CanopyThickness ), nameof( s.CanopyOverhang ), nameof( s.CanopyPosts ), nameof( s.CanopyPostWidth )] );
else if ( !s.CanopyPosts )
h.UnionWith( [nameof( s.CanopyPostWidth ), nameof( s.CanopyPostMaterial )] );
}
internal static void HideRoof( HashSet<string> h, RoofStudioSettings s )
{
if ( s.IsFlat )
h.Add( nameof( s.Hollow ) );
if ( s.IsFlat || !s.Hollow )
h.UnionWith( [nameof( s.BottomClosure ), nameof( s.BottomThickness ), nameof( s.GableEnds ), nameof( s.GableThickness )] );
else
{
if ( !s.BottomClosure )
h.Add( nameof( s.BottomThickness ) );
if ( s.Style == StudioRoofStyle.Hip )
h.Add( nameof( s.GableEnds ) );
if ( s.Style == StudioRoofStyle.Hip || !s.GableEnds )
h.Add( nameof( s.GableThickness ) );
}
// The margin is set by snapping onto walls; as a value it only sets how thick closed gables are.
if ( s.IsFlat || !s.Hollow || !s.GableEnds || s.Style == StudioRoofStyle.Hip || s.GableThickness > 0 )
h.Add( nameof( s.WallMargin ) );
}
internal static void Facade( ArchitectureStudioPlan p, FacadeWallStudioSettings s, float w, float h, float thick, float skin )
{
// Fins and cornices stand on the outer face of the finish and only touch it. A classical bay
// has mouldings up to 2 out and its cornice 3 out; fins stand on those.
var face = skin * s.Relief;
var classical = s.Style == StudioWallStyle.Classical;
var rows = s.CorniceRows();
var cornice = Math.Clamp( s.CorniceProjection, 1, 24 );
var band = s.BandRange;
var bandOut = Math.Clamp( s.BandProjection, .5f, 16 );
float Standoff( float z0, float z1 )
{
var d = classical ? 2.0f : 0;
if ( classical && z1 > h - s.ClassicalCornice + .001f )
d = MathF.Max( d, 3 );
if ( s.Band && z1 > band.Low + .001f && z0 < band.High - .001f )
d = MathF.Max( d, bandOut );
foreach ( var (low, high) in rows )
if ( z1 > low + .001f && z0 < high - .001f )
d = MathF.Max( d, cornice );
return d;
}
foreach ( var back in s.BothSides ? new[]
{
false,
true
}
: new[]
{
false
}
)
{
var id = back ? "back" : "front";
if ( s.Fins )
{
var count = Math.Clamp( (int)MathF.Ceiling( w / Math.Clamp( s.FinSpacing, 8, 128 ) ), 1, 128 );
var step = w / count;
var fw = MathF.Min( Math.Clamp( s.FinWidth, .5f, 8 ), step * .5f );
var projection = Math.Clamp( s.FinProjection, 1, 32 );
// Split the fin where the band or a cornice crosses it; there it stands in front of them.
var cuts = new List<float> { 0, h };
if ( s.Band )
cuts.AddRange( [band.Low, band.High] );
foreach ( var (low, high) in rows )
cuts.AddRange( [low, high] );
if ( classical )
cuts.Add( h - s.ClassicalCornice );
var levels = cuts.Where( z => z >= 0 && z <= h ).Distinct().OrderBy( z => z ).ToList();
p.Add( id + "_fins", "Vertical Facade Fins", p.Detail, k =>
{
for ( var i = 0; i < count; i++ )
{
var x = (i + .5f) * step;
for ( var j = 0; j < levels.Count - 1; j++ )
{
var (z0, z1) = (levels[j], levels[j + 1]);
var near = face + Standoff( z0, z1 );
var far = face + MathF.Max( projection, Standoff( z0, z1 ) + .5f );
if ( z1 - z0 < .01f )
continue;
k.Box( new( x - fw * .5f, back ? thick + near : -far, z0 ), new( x + fw * .5f, back ? thick + far : -near, z1 ) );
}
}
} );
}
if ( s.RepeatedBands )
{
if ( rows.Count > 0 )
p.Add( id + "_cornices", "Repeated Cornices", p.Detail, k =>
{
foreach ( var (low, high) in rows )
k.Box( new( 0, back ? thick + face : -face - cornice, low ), new( w, back ? thick + face + cornice : -face, high ) );
} );
}
}
}
internal static void Balcony( ArchitectureStudioPlan p, BalconyStudioSettings s, float w, float d, float t, GenContext context )
{
if ( !s.Canopy )
return;
var height = MathF.Max( Math.Clamp( s.CanopyHeight, 64, 256 ), Math.Clamp( s.RailingHeight, 12, 128 ) + 16 );
var z = t + height;
var over = Math.Clamp( s.CanopyOverhang, 0, 32 );
p.Box( "canopy", "Balcony Canopy", context.Material( s.CanopyMaterial ?? s.StructureMaterial ), new( -over, 0, z ), new( w + over, d + over, z + Math.Clamp( s.CanopyThickness, 1, 16 ) ) );
if ( s.CanopyPosts )
{
var a = MathF.Min( Math.Clamp( s.CanopyPostWidth, 2, 12 ), MathF.Min( w, d ) * .2f );
// Where a railing runs along the corner, the post stands on its handrail instead of cutting through it.
var rail = t + Math.Clamp( s.RailingHeight, 12, 128 );
var leftFoot = s.Front || s.Left && !s.PrivacyLeft ? rail : t;
var rightFoot = s.Front || s.Right && !s.PrivacyRight ? rail : t;
if ( s.PrivacyLeft )
leftFoot = MathF.Max( leftFoot, t + Math.Clamp( s.PrivacyHeight, 12, 192 ) );
if ( s.PrivacyRight )
rightFoot = MathF.Max( rightFoot, t + Math.Clamp( s.PrivacyHeight, 12, 192 ) );
if ( z - leftFoot > .5f )
p.Box( "canopy_post_left", "Left Canopy Post", context.Material( s.CanopyPostMaterial ?? s.DetailMaterial ?? s.StructureMaterial ), new( 0, d - a, leftFoot ), new( a, d, z ) );
if ( z - rightFoot > .5f )
p.Box( "canopy_post_right", "Right Canopy Post", context.Material( s.CanopyPostMaterial ?? s.DetailMaterial ?? s.StructureMaterial ), new( w - a, d - a, rightFoot ), new( w, d, z ) );
}
}
internal static void RoofShell( ArchitectureStudioPlan p, RoofStudioSettings s, GenContext context, float thickness )
{
var shape = s.Style switch
{
StudioRoofStyle.Hip => HouseRoof.Hip,
StudioRoofStyle.Shed => HouseRoof.Shed,
StudioRoofStyle.Butterfly => HouseRoof.Butterfly,
_ => HouseRoof.Gable
};
var shell = RoofGenerator.Generate( new RoofRequest( "shell", "Hollow Roof Shell", MathF.Max( s.Width, 16 ), MathF.Max( s.Depth, 16 ), Math.Clamp( s.Rise, 1, 1024 ), s.EaveOverhang, thickness, shape, 128, false, 0, p.Structure, p.Detail, false, false, s.EndOverhang, s.GableWall ), context );
shell.Children.RemoveAll( c => c.Kind == "Rafter" || (!s.GableEnds && c.Kind == "GableEnd") );
shell.Local = new Transform( Vector3.Up * s.Elevation );
p.Root.Add( shell );
if ( s.BottomClosure )
p.Box( "ceiling", "Removable Bottom Closure", p.Finish, new( 0, 0, -Math.Clamp( s.BottomThickness, .5f, 32 ) ), new( MathF.Max( s.Width, 16 ), MathF.Max( s.Depth, 16 ), 0 ) );
}
}