Editor tool and part generator for facade walls. Exposes settings for wall style (plain, panels, brick, stone, slats, classical), cladding, trims, piers, bands and slats, then builds a PartPlan of boxes for the core, cladding, mouldings and classical panels. The editor class wires it into the line tool for preview and placement.
namespace AdvancedMappingTools;
public enum StudioWallStyle
{
Plain,
Panels,
Brick,
Stone,
Slats,
Classical
}
public enum StudioSlatDirection
{
Vertical,
Horizontal
}
public sealed partial class FacadeWallStudioSettings : ArchitectureStudioSettings
{
[Property, Group( "Wall" ), WideMode]
public StudioWallStyle Style { get; set; } = StudioWallStyle.Panels;
[Property, Group( "Wall" ), Range( 16, 1024, slider: false ), WideMode]
public float Height { get; set; } = 128;
[Property, Group( "Wall" ), Range( 2, 64, slider: false ), WideMode]
public float Thickness { get; set; } = 8;
[Property, Group( "Cladding" ), Title( "Cladding Depth" ), Range( .5f, 16, slider: false ), WideMode]
public float CladdingDepth { get; set; } = 2;
[Property, Group( "Cladding" ), Title( "Module Width" ), Range( 4, 128, slider: false ), WideMode]
public float ModuleWidth { get; set; } = 32;
[Property, Group( "Cladding" ), Title( "Row Height" ), Range( 4, 128, slider: false ), WideMode]
public float RowHeight { get; set; } = 16;
[Property, Group( "Cladding" ), Title( "Joint / Gap" ), Range( .25f, 8, slider: false ), WideMode]
public float Joint { get; set; } = 1;
[Property, Group( "Cladding" ), Title( "Both Sides" ), WideMode]
public bool BothSides { get; set; }
[Property, Group( "Classical" ), Title( "Panel Inset" ), Range( 2, 24, slider: false ), WideMode]
public float PanelInset { get; set; } = 8;
[Property, Group( "Classical" ), Title( "Moulding Width" ), Range( .5f, 8, slider: false ), WideMode]
public float MouldingWidth { get; set; } = 2;
[Property, Group( "Trims" ), Title( "Base Height" ), Range( 0, 64, slider: false ), WideMode]
public float BaseHeight { get; set; } = 8;
[Property, Group( "Trims" ), Title( "Cap Height" ), Range( 0, 32, slider: false ), WideMode]
public float CapHeight { get; set; } = 4;
[Property, Group( "Trims" ), Title( "Cap Overhang" ), Range( 0, 16, slider: false ), WideMode]
public float CapOverhang { get; set; } = 2;
[Property, Group( "Piers" ), WideMode]
public bool Piers { get; set; } = true;
[Property, Group( "Piers" ), Title( "Pier Spacing" ), Range( 16, 256, slider: false ), WideMode]
public float PierSpacing { get; set; } = 96;
[Property, Group( "Piers" ), Title( "Pier Width" ), Range( 2, 32, slider: false ), WideMode]
public float PierWidth { get; set; } = 6;
[Property, Group( "Facade Band" ), Title( "Horizontal Band" ), WideMode]
public bool Band { get; set; }
[Property, Group( "Facade Band" ), Title( "Band Position (%)" ), Range( 0, 100, slider: false ), WideMode]
public float BandPosition { get; set; } = 60;
[Property, Group( "Facade Band" ), Title( "Band Height" ), Range( 1, 32, slider: false ), WideMode]
public float BandHeight { get; set; } = 6;
[Property, Group( "Facade Band" ), Title( "Band Projection" ), Range( .5f, 16, slider: false ), WideMode]
public float BandProjection { get; set; } = 2;
[Property, Group( "Slats" ), Title( "Slat Direction" ), WideMode]
public StudioSlatDirection SlatDirection { get; set; } = StudioSlatDirection.Vertical;
/// <summary>Every second slat stands out this many times the cladding depth; 1 keeps all slats flush.</summary>
[Property, Group( "Slats" ), Title( "Slat Relief" ), Range( 1, 3, slider: false ), WideMode]
public float SlatRelief { get; set; } = 1.6f;
internal float Relief => Style == StudioWallStyle.Slats ? Math.Clamp( float.IsFinite( SlatRelief ) ? SlatRelief : 1, 1, 3 ) : Style == StudioWallStyle.Stone ? 1.4f : 1;
// Heights shared by the generator, the classical bays and the extras, so they never disagree.
internal float WallHeight => Math.Clamp( Height, 16, 1024 );
internal float Foot => MathF.Min( Math.Clamp( BaseHeight, 0, 64 ), WallHeight * .25f );
internal float ClassicalCornice => Style == StudioWallStyle.Classical ? MathF.Min( 8.0f, (WallHeight - Foot) * .12f ) : 0;
internal (float Low, float High) BandRange
{
get
{
var h = WallHeight;
var height = MathF.Min( Math.Clamp( BandHeight, 1, 32 ), h - Foot );
var low = Foot + (h - Foot - height) * Math.Clamp( BandPosition, 0, 100 ) / 100;
return (low, low + height);
}
}
/// <summary>The repeated cornices that are built: none on the band, none in the classical cornice.</summary>
internal List<(float Low, float High)> CorniceRows()
{
var rows = new List<(float Low, float High)>();
if ( !RepeatedBands )
return rows;
var spacing = Math.Clamp( CorniceSpacing, 16, 256 );
var height = MathF.Min( Math.Clamp( CorniceHeight, 1, 16 ), spacing * .5f );
var top = WallHeight - ClassicalCornice;
var band = BandRange;
for ( var z = spacing; z < WallHeight; z += spacing )
{
if ( z > top + .001f || z - height < Foot - .001f )
continue;
if ( Band && z > band.Low && z - height < band.High )
continue;
rows.Add( (z - height, z) );
}
return rows;
}
public override IReadOnlySet<string> UnusedOptions
{
get
{
var set = new HashSet<string>( base.UnusedOptions );
StudioExtras.HideFacade( set, this );
if ( !Band )
set.UnionWith( [nameof( BandPosition ), nameof( BandHeight ), nameof( BandProjection )] );
if ( !Piers )
{
set.Add( nameof( PierSpacing ) );
set.Add( nameof( PierWidth ) );
}
if ( Style is StudioWallStyle.Plain or StudioWallStyle.Classical )
foreach ( var n in new[]
{
nameof( ModuleWidth ),
nameof( RowHeight ),
nameof( Joint )
}
)
set.Add( n );
if ( Style == StudioWallStyle.Slats )
set.Add( SlatDirection == StudioSlatDirection.Horizontal ? nameof( ModuleWidth ) : nameof( RowHeight ) );
if ( Style != StudioWallStyle.Slats )
set.UnionWith( [nameof( SlatDirection ), nameof( SlatRelief )] );
if ( Style != StudioWallStyle.Classical )
{
set.Add( nameof( PanelInset ) );
set.Add( nameof( MouldingWidth ) );
}
return set;
}
}
}
public sealed class FacadeWallStudioGenerator : IPartGenerator
{
public string Id => "facade_wall_studio";
public string Title => "Facade Wall Studio";
public Type SettingsType => typeof(FacadeWallStudioSettings);
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (FacadeWallStudioSettings)settings;
var p = new ArchitectureStudioPlan( context, s, Title );
var w = MathF.Max( s.Width, 16 );
var h = Math.Clamp( s.Height, 16, 1024 );
var thick = Math.Clamp( s.Thickness, 2, 64 );
var skin = Math.Clamp( s.CladdingDepth, .5f, 16 );
var foot = s.Foot;
var cap = MathF.Min( Math.Clamp( s.CapHeight, 0, 32 ), h * .25f );
var over = Math.Clamp( s.CapOverhang, 0, 16 );
p.Box( "core", "Wall Core", p.Structure, new( 0, 0, 0 ), new( w, thick, h ) );
if ( foot > 0 )
p.Box( "base", "Base Moulding", p.Detail, new( 0, -skin, 0 ), new( w, 0, foot ) );
if ( cap > 0 )
p.Box( "cap", "Wall Cap", p.Detail, new( -over, -skin - over, h ), new( w + over, thick + skin + over, h + cap ) );
var bays = s.Piers ? Math.Clamp( (int)MathF.Ceiling( w / Math.Clamp( s.PierSpacing, 16, 256 ) ), 1, 64 ) : 1;
var pier = s.Piers ? MathF.Min( Math.Clamp( s.PierWidth, 2, 32 ), w / (bays + 1) * .5f ) : 0;
var bandLow = s.BandRange.Low;
var bandHeight = s.BandRange.High - s.BandRange.Low;
// Cladding and piers stop at the band, so the band only touches them.
void Clad( MeshKit kit, Vector3 a, Vector3 b )
{
if ( !s.Band || b.z <= bandLow || a.z >= bandLow + bandHeight )
{
kit.Box( a, b );
return;
}
if ( bandLow - a.z > .01f )
kit.Box( a, b.WithZ( bandLow ) );
if ( b.z - (bandLow + bandHeight) > .01f )
kit.Box( a.WithZ( bandLow + bandHeight ), b );
}
void Side( bool back )
{
var prefix = back ? "back" : "front";
var y0 = back ? thick : -skin;
var y1 = back ? thick + skin : 0;
if ( back && foot > 0 )
p.Box( "back_base", "Back Base Moulding", p.Detail, new( 0, y0, 0 ), new( w, y1, foot ) );
var sections = new List<(float start, float end)>();
if ( s.Piers )
{
var slot = (w - pier) / bays;
for ( var i = 0; i <= bays; i++ )
{
var x = i * slot;
p.Add( prefix + "_pier" + i, "Facade Pier", p.Detail, kit => Clad( kit, new( x, y0, foot ), new( x + pier, y1, h ) ) );
}
for ( var i = 0; i < bays; i++ )
sections.Add( (i * slot + pier, (i + 1) * slot) );
}
else
sections.Add( (0, w) );
for ( var bay = 0; bay < sections.Count; bay++ )
{
var(start, end) = sections[bay];
var bh = h - foot;
p.Add( prefix + "_cladding" + bay, "Facade Finish", p.Finish, kit =>
{
if ( s.Style is StudioWallStyle.Plain or StudioWallStyle.Classical )
{
Clad( kit, new( start, y0, foot ), new( end, y1, h ) );
return;
}
if ( s.Style == StudioWallStyle.Slats && s.SlatDirection == StudioSlatDirection.Horizontal )
{
// Horizontal boards across the bay, a shadow gap between, every second one standing out.
var rows = Math.Clamp( (int)MathF.Ceiling( bh / MathF.Max( Math.Clamp( s.RowHeight, 4, 128 ) / 3.0f, 4 ) ), 1, 128 );
var board = bh / rows;
var shadow = MathF.Min( MathF.Max( Math.Clamp( s.Joint, .25f, 8 ), board * .18f ), board * .4f );
for ( var row = 0; row < rows; row++ )
{
var relief = row % 2 == 1 ? s.Relief : 1;
var z0 = foot + row * board + (row == 0 ? 0 : shadow * .5f);
var z1 = foot + (row + 1) * board - (row == rows - 1 ? 0 : shadow * .5f);
Clad( kit, new( start, back ? thick : -skin * relief, z0 ), new( end, back ? thick + skin * relief : 0, z1 ) );
}
return;
}
var pitch = Math.Clamp( s.ModuleWidth, 4, 128 ) / (s.Style == StudioWallStyle.Slats ? 3.0f : 1.0f);
var nx = Math.Clamp( (int)MathF.Ceiling( (end - start) / MathF.Max( pitch, 4 ) ), 1, Math.Max( 1, 128 / bays ) );
var ny = s.Style == StudioWallStyle.Slats ? 1 : Math.Clamp( (int)MathF.Ceiling( bh / Math.Clamp( s.RowHeight, 4, 128 ) ), 1, 64 );
var sx = (end - start) / nx;
var sz = bh / ny;
var gap = MathF.Min( Math.Clamp( s.Joint, .25f, 8 ), MathF.Min( sx, sz ) * .4f );
// Slats: a shadow gap between two boards, wide enough to read from afar.
if ( s.Style == StudioWallStyle.Slats )
gap = MathF.Min( MathF.Max( gap, sx * .18f ), sx * .4f );
for ( var row = 0; row < ny; row++ )
{
var shift = s.Style is StudioWallStyle.Brick or StudioWallStyle.Stone && row % 2 == 1 ? sx * .5f : 0;
for ( var i = -1; i <= nx; i++ )
{
var x0 = MathF.Max( start, start + i * sx + shift + gap * .5f );
var x1 = MathF.Min( end, start + (i + 1) * sx + shift - gap * .5f );
if ( x1 - x0 < .1f )
continue;
var z0 = foot + row * sz + (s.Style == StudioWallStyle.Slats ? 0 : gap * .5f);
var z1 = foot + (row + 1) * sz - (s.Style == StudioWallStyle.Slats ? 0 : gap * .5f);
var relief = s.Style == StudioWallStyle.Stone ? 1 + ((row + i + 10) % 3) * .2f : s.Style == StudioWallStyle.Slats && (i + 10) % 2 == 1 ? s.Relief : 1;
Clad( kit, new( x0, back ? thick : -skin * relief, z0 ), new( x1, back ? thick + skin * relief : 0, z1 ) );
}
}
} );
if ( s.Style == StudioWallStyle.Classical )
Classical( p, s, prefix, bay, start, end, foot, h, bh, back, back ? thick + skin : -skin, bandLow, bandHeight );
}
}
if ( s.Band )
{
var height = bandHeight;
var low = bandLow;
// Extend into the cladding to meet the structural core, including across joints.
var face = skin * s.Relief;
var projection = Math.Clamp( s.BandProjection, .5f, 16 );
p.Box( "band_front", "Front Facade Band", p.Detail, new( 0, -face - projection, low ), new( w, 0, low + height ) );
if ( s.BothSides )
p.Box( "band_back", "Back Facade Band", p.Detail, new( 0, thick, low ), new( w, thick + face + projection, low + height ) );
}
Side( false );
if ( s.BothSides )
Side( true );
StudioExtras.Facade( p, s, w, h, thick, skin );
return p.Root;
}
/// <summary>
/// A classical bay: a stepped cornice under the cap, a dado rail at a third of the height, and
/// a raised panel in a moulded frame below and above it. <paramref name="face"/> is the outer
/// face of the finish; everything stands out from it, away from the wall.
/// </summary>
static void Classical( ArchitectureStudioPlan p, FacadeWallStudioSettings s, string prefix, int bay, float start, float end, float foot, float h, float bh, bool back, float face, float bandLow, float bandHeight )
{
var outward = back ? 1.0f : -1.0f;
Vector3 At( float x, float depth, float z ) => new( x, face + outward * depth, z );
void Slab( MeshKit kit, float x0, float x1, float d0, float d1, float z0, float z1 )
{
var a = At( x0, d0, z0 );
var c = At( x1, d1, z1 );
kit.Box( Vector3.Min( a, c ), Vector3.Max( a, c ) );
}
// Cornice: two steps, the upper one standing further out.
var cornice = s.ClassicalCornice;
// With a facade band, the band is the dado rail; a band up at the cap replaces the cornice.
if ( s.Band && bandLow + bandHeight > h - cornice )
cornice = 0;
if ( cornice > 0 )
p.Add( prefix + "_cornice" + bay, "Classical Cornice", p.Detail, kit =>
{
Slab( kit, start, end, 0, 1.5f, h - cornice, h - cornice * .45f );
Slab( kit, start, end, 0, 3.0f, h - cornice * .45f, h );
} );
if ( end - start <= 12 ) return;
var inset = MathF.Min( Math.Clamp( s.PanelInset, 2, 24 ), MathF.Min( end - start, bh ) * .2f );
var a = start + inset;
var c = end - inset;
var m = MathF.Min( Math.Clamp( s.MouldingWidth, .5f, 8 ), (c - a) * .15f );
// The band and the repeated cornices divide the bay; without them a dado rail sits at a third.
var dividers = s.CorniceRows();
if ( s.Band )
dividers.Add( (bandLow, bandLow + bandHeight) );
var dado = foot + bh * .33f;
var rail = MathF.Min( 3.0f, bh * .04f );
if ( dividers.Count == 0 )
{
dividers.Add( (dado, dado + rail) );
p.Box( prefix + "_dado" + bay, "Dado Rail", p.Detail, Vector3.Min( At( start, 0, dado ), At( end, 2.0f, dado + rail ) ), Vector3.Max( At( start, 0, dado ), At( end, 2.0f, dado + rail ) ) );
}
dividers.Sort();
var fields = new List<(float Low, float High)>();
var below = foot + inset * .5f;
foreach ( var (low, high) in dividers )
{
if ( low - inset * .5f - below > m * 3 )
fields.Add( (below, low - inset * .5f) );
below = high + inset;
}
if ( h - cornice - inset - below > m * 3 )
fields.Add( (below, h - cornice - inset) );
p.Add( prefix + "_moulding" + bay, "Classical Panel Moulding", p.Detail, kit =>
{
foreach ( var (low, high) in fields )
{
Slab( kit, a, c, 0, 1.5f, low, low + m );
Slab( kit, a, c, 0, 1.5f, high - m, high );
Slab( kit, a, a + m, 0, 1.5f, low + m, high - m );
Slab( kit, c - m, c, 0, 1.5f, low + m, high - m );
}
} );
p.Add( prefix + "_panels" + bay, "Raised Panels", p.Finish, kit =>
{
foreach ( var (low, high) in fields )
Slab( kit, a + m, c - m, 0, 0.75f, low + m, high - m );
} );
}
}
[ToolCategory( "Walls & Floors" ), Title( "Facade Wall Studio" ), Icon( "view_week" )]
public sealed class FacadeWallStudioEditor( PrimitiveTool tool ) : LineEditor( tool )
{
readonly FacadeWallStudioGenerator generator = new();
public FacadeWallStudioSettings Settings { get; } = new();
public override object ExtraSettings => Settings;
protected override bool PlacesObjects => true;
public override bool ChangesOtherSettings( string property ) => property is nameof( FacadeWallStudioSettings.Fins ) or nameof( FacadeWallStudioSettings.RepeatedBands ) or nameof( FacadeWallStudioSettings.Style ) or nameof( FacadeWallStudioSettings.Piers ) or nameof( FacadeWallStudioSettings.Band ) or nameof( FacadeWallStudioSettings.SlatDirection );
void Prepare()
{
Settings.Width = Length;
Settings.StructureMaterial = PartTool.Pick( Settings.StructureMaterial, SurfaceMaterial );
Settings.DetailMaterial = PartTool.Pick( Settings.DetailMaterial, TrimMaterial );
Settings.FinishMaterial = PartTool.Pick( Settings.FinishMaterial, SurfaceMaterial );
}
protected override void BuildMesh( MeshKit kit )
{
}
protected override PolygonMesh BuildPreview()
{
Prepare();
return PartService.Plan( generator, Settings ).Flatten( LineFrame );
}
protected override void PlaceObjects()
{
Prepare();
PartTool.Place( generator, GeneratorRegistry.Load( generator, GeneratorRegistry.Save( Settings ) ), LineFrame );
}
}