Editor tooling for architecture primitives. Defines a Shell helper that builds four walls with door/window openings and trim frames, plus Room, Building and Bunker editors that generate meshes from a rectangle.
namespace AdvancedMappingTools;
public enum RoomSide
{
None,
Front,
Right,
Back,
Left
}
/// <summary>
/// Walls around a rectangle, drawn counter clockwise with their insides to the left.
/// </summary>
public static class Shell
{
public record struct Openings( IReadOnlyList<(float Start, float End)> Spans, float Sill, float Head, ArchStyle Arch );
public static List<(float Start, float End)> Even( float length, int count, float width )
{
var result = new List<(float, float)>( count );
if ( count <= 0 ) return result;
var slot = length / count;
width = Math.Clamp( width, 2.0f, MathF.Max( 2.0f, slot - 2.0f ) );
for ( var i = 0; i < count; i++ )
{
var center = slot * (i + 0.5f);
result.Add( (center - width * 0.5f, center + width * 0.5f) );
}
return result;
}
/// <summary>
/// Add four walls. The walls of side 1 and 3 fit between those of side 0 and 2.
/// <paramref name="openings"/> gets the side and the free length of its wall.
/// </summary>
public static void Walls( MeshKit kit, Transform frame, float length, float depth, float thickness, float z, float height,
Func<int, float, Openings> openings, float frameWidth, Material surface, Material trim )
{
var half = thickness * 0.5f;
(Vector3 Corner, float Yaw, float Length)[] sides =
[
(new Vector3( 0.0f, half, z ), 0.0f, length),
(new Vector3( length - half, thickness, z ), 90.0f, depth - thickness * 2.0f),
(new Vector3( length, depth - half, z ), 180.0f, length),
(new Vector3( half, depth - thickness, z ), 270.0f, depth - thickness * 2.0f)
];
for ( var i = 0; i < sides.Length; i++ )
{
var (corner, yaw, span) = sides[i];
if ( span <= 1.0f ) continue;
var open = openings( i, span );
kit.Frame = frame.ToWorld( new Transform( corner, Rotation.FromYaw( yaw ) ) );
kit.Material = surface;
if ( open.Spans.Count == 0 )
{
kit.WallWithOpenings( span, thickness, height, open.Spans, 0.0f, height );
continue;
}
var width = open.Spans[0].End - open.Spans[0].Start;
var rise = MathF.Min( MeshKit.ArchRise( open.Arch, width ), open.Head - open.Sill - 1.0f );
var depthOut = half + 1.0f;
var flat = open.Arch == ArchStyle.Flat || rise <= 0.0f;
var head = MathF.Min( open.Head, height - 1.0f );
if ( flat ) kit.WallWithOpenings( span, thickness, height, open.Spans, open.Sill, head );
else kit.WallWithArches( span, thickness, height, open.Spans, open.Sill, head - rise, rise, open.Arch, 12 );
kit.Material = trim;
foreach ( var (x0, x1) in open.Spans )
{
if ( flat ) kit.OpeningFrame( x0, x1, open.Sill, head, frameWidth, depthOut, open.Sill > 0.0f );
else kit.ArchFrame( x0, x1, open.Sill, head - rise, rise, open.Arch, 12, frameWidth, depthOut, open.Sill > 0.0f );
}
}
kit.Frame = frame;
}
}
/// <summary>
/// A closed room with floor, ceiling, walls, a door and windows, in three clicks.
/// </summary>
[ToolCategory( "Buildings" )]
[Title( "Room" ), Icon( "meeting_room" )]
public sealed class RoomEditor( PrimitiveTool tool ) : RectEditor( tool )
{
[Property, Group( "Room" ), Range( 8, 2048, slider: false ), WideMode]
public float Height { get; set; } = 128.0f;
[Property, Group( "Room" ), Title( "Wall Thickness" ), Range( 1, 128, slider: false ), WideMode]
public float WallThickness { get; set; } = 8.0f;
[Property, Group( "Room" ), WideMode]
public bool Floor { get; set; } = true;
[Property, Group( "Room" ), WideMode]
public bool Ceiling { get; set; } = true;
[Property, Group( "Room" ), Title( "Slab Thickness" ), Range( 1, 128, slider: false ), WideMode]
public float SlabThickness { get; set; } = 8.0f;
[Property, Group( "Openings" ), Title( "Door Side" ), WideMode]
public RoomSide DoorSide { get; set; } = RoomSide.Front;
[Property, Group( "Openings" ), Title( "Door Width" ), Range( 8, 512, slider: false ), WideMode]
public float DoorWidth { get; set; } = 56.0f;
[Property, Group( "Openings" ), Title( "Door Height" ), Range( 8, 512, slider: false ), WideMode]
public float DoorHeight { get; set; } = 96.0f;
[Property, Group( "Openings" ), Title( "Windows Per Side" ), Range( 0, 16, slider: false ), WideMode]
public int WindowsPerSide { get; set; } = 1;
[Property, HideIf( nameof( WindowsPerSide ), 0 ), Group( "Openings" ), Title( "Window Width" ), Range( 8, 512, slider: false ), WideMode]
public float WindowWidth { get; set; } = 48.0f;
[Property, HideIf( nameof( WindowsPerSide ), 0 ), Group( "Openings" ), Title( "Window Height" ), Range( 8, 512, slider: false ), WideMode]
public float WindowHeight { get; set; } = 48.0f;
[Property, HideIf( nameof( WindowsPerSide ), 0 ), Group( "Openings" ), Title( "Sill Height" ), Range( 1, 512, slider: false ), WideMode]
public float SillHeight { get; set; } = 36.0f;
[Property, HideIf( nameof( WindowsPerSide ), 0 ), Group( "Openings" ), Title( "Window Arch" ), WideMode]
public ArchStyle WindowArch { get; set; } = ArchStyle.Flat;
protected override void BuildMesh( MeshKit kit )
{
var frame = RectFrame;
var length = RectLength;
var depth = RectDepth;
Shell.Walls( kit, frame, length, depth, WallThickness, 0.0f, Height, ( side, span ) =>
{
if ( (int)DoorSide - 1 == side )
return new Shell.Openings( Shell.Even( span, 1, DoorWidth ), 0.0f, DoorHeight, ArchStyle.Flat );
return new Shell.Openings( Shell.Even( span, WindowsPerSide, WindowWidth ), SillHeight, SillHeight + WindowHeight, WindowArch );
}, 3.0f, SurfaceMaterial, TrimMaterial );
kit.Material = TrimMaterial;
if ( Floor ) kit.Box( new Vector3( 0.0f, 0.0f, -SlabThickness ), new Vector3( length, depth, 0.0f ) );
if ( Ceiling ) kit.Box( new Vector3( 0.0f, 0.0f, Height ), new Vector3( length, depth, Height + SlabThickness ) );
}
}
/// <summary>
/// A whole building shell: foundation, storeys with windows, an entrance and a roof.
/// </summary>
[ToolCategory( "Buildings" )]
[Title( "Building" ), Icon( "apartment" )]
public sealed class BuildingEditor( PrimitiveTool tool ) : RectEditor( tool )
{
[Property, Group( "Building" ), Range( 1, 64, slider: false ), WideMode]
public int Storeys { get; set; } = 2;
[Property, Group( "Building" ), Title( "Storey Height" ), Range( 32, 1024, slider: false ), WideMode]
public float StoreyHeight { get; set; } = 128.0f;
[Property, Group( "Building" ), Title( "Wall Thickness" ), Range( 1, 128, slider: false ), WideMode]
public float WallThickness { get; set; } = 8.0f;
[Property, Group( "Building" ), Title( "Slab Thickness" ), Range( 1, 64, slider: false ), WideMode]
public float SlabThickness { get; set; } = 8.0f;
[Property, Group( "Building" ), Range( 0, 256, slider: false ), WideMode]
public float Foundation { get; set; } = 8.0f;
[Property, Group( "Openings" ), Title( "Door Width" ), Range( 8, 512, slider: false ), WideMode]
public float DoorWidth { get; set; } = 64.0f;
[Property, Group( "Openings" ), Title( "Door Height" ), Range( 8, 512, slider: false ), WideMode]
public float DoorHeight { get; set; } = 96.0f;
[Property, Group( "Openings" ), Title( "Window Spacing" ), Range( 32, 1024, slider: false ), WideMode]
public float WindowSpacing { get; set; } = 96.0f;
[Property, Group( "Openings" ), Title( "Window Width" ), Range( 8, 512, slider: false ), WideMode]
public float WindowWidth { get; set; } = 40.0f;
[Property, Group( "Openings" ), Title( "Window Height" ), Range( 8, 512, slider: false ), WideMode]
public float WindowHeight { get; set; } = 56.0f;
[Property, Group( "Openings" ), Title( "Sill Height" ), Range( 1, 512, slider: false ), WideMode]
public float SillHeight { get; set; } = 36.0f;
[Property, Group( "Openings" ), Title( "Window Arch" ), WideMode]
public ArchStyle WindowArch { get; set; } = ArchStyle.Flat;
[Property, Group( "Roof" ), Title( "Roof Shape" ), WideMode]
public RoofShape RoofShape { get; set; } = RoofShape.Gable;
[Property, Group( "Roof" ), Title( "Ridge Height" ), Range( 0, 1024, slider: false ), WideMode]
public float RidgeHeight { get; set; } = 72.0f;
[Property, Group( "Roof" ), Range( 0, 128, slider: false ), WideMode]
public float Overhang { get; set; } = 12.0f;
[Property, Group( "Materials" ), Title( "Roof" ), WideMode]
public Material RoofMaterial { get; set; }
protected override void BuildMesh( MeshKit kit )
{
var frame = RectFrame;
var length = RectLength;
var depth = RectDepth;
var z = 0.0f;
kit.Frame = frame;
kit.Material = TrimMaterial;
if ( Foundation > 0.0f )
{
kit.Box( new Vector3( -2.0f, -2.0f, 0.0f ), new Vector3( length + 2.0f, depth + 2.0f, Foundation ) );
z = Foundation;
}
for ( var storey = 0; storey < Storeys; storey++ )
{
var current = storey;
Shell.Walls( kit, frame, length, depth, WallThickness, z, StoreyHeight, ( side, span ) =>
{
if ( current == 0 && side == 0 )
return new Shell.Openings( Shell.Even( span, 1, DoorWidth ), 0.0f, DoorHeight, ArchStyle.Flat );
var count = (int)MathF.Floor( span / MathF.Max( WindowSpacing, 32.0f ) );
return new Shell.Openings( Shell.Even( span, count, WindowWidth ), SillHeight, SillHeight + WindowHeight, WindowArch );
}, 2.0f, SurfaceMaterial, TrimMaterial );
z += StoreyHeight;
kit.Frame = frame;
kit.Material = TrimMaterial;
kit.Box( new Vector3( 0.0f, 0.0f, z ), new Vector3( length, depth, z + SlabThickness ) );
z += SlabThickness;
}
var top = frame.ToWorld( new Transform( Vector3.Up * z ) );
kit.Frame = top;
RoofEditor.BuildRoof( kit, top, -Overhang, length + Overhang, -Overhang, depth + Overhang,
RoofShape, RidgeHeight, 4.0f, RoofMaterial ?? SurfaceMaterial, TrimMaterial );
}
}
/// <summary>
/// A low concrete bunker with firing slits and a thick roof.
/// </summary>
[ToolCategory( "Buildings" )]
[Title( "Bunker" ), Icon( "shield" )]
public sealed class BunkerEditor( PrimitiveTool tool ) : RectEditor( tool )
{
[Property, Group( "Bunker" ), Range( 32, 512, slider: false ), WideMode]
public float Height { get; set; } = 96.0f;
[Property, Group( "Bunker" ), Title( "Wall Thickness" ), Range( 4, 128, slider: false ), WideMode]
public float WallThickness { get; set; } = 24.0f;
[Property, Group( "Bunker" ), Title( "Roof Thickness" ), Range( 4, 128, slider: false ), WideMode]
public float RoofThickness { get; set; } = 24.0f;
[Property, Group( "Bunker" ), Title( "Slits Per Side" ), Range( 0, 16, slider: false ), WideMode]
public int SlitsPerSide { get; set; } = 2;
[Property, HideIf( nameof( SlitsPerSide ), 0 ), Group( "Bunker" ), Title( "Slit Width" ), Range( 8, 256, slider: false ), WideMode]
public float SlitWidth { get; set; } = 64.0f;
[Property, HideIf( nameof( SlitsPerSide ), 0 ), Group( "Bunker" ), Title( "Slit Height" ), Range( 4, 128, slider: false ), WideMode]
public float SlitHeight { get; set; } = 12.0f;
[Property, Group( "Bunker" ), Title( "Door Width" ), Range( 16, 256, slider: false ), WideMode]
public float DoorWidth { get; set; } = 48.0f;
protected override void BuildMesh( MeshKit kit )
{
var frame = RectFrame;
var length = RectLength;
var depth = RectDepth;
var sill = Height * 0.55f;
Shell.Walls( kit, frame, length, depth, WallThickness, 0.0f, Height, ( side, span ) => side == 2
? new Shell.Openings( Shell.Even( span, 1, DoorWidth ), 0.0f, MathF.Min( 80.0f, Height - 4.0f ), ArchStyle.Flat )
: new Shell.Openings( Shell.Even( span, SlitsPerSide, SlitWidth ), sill, sill + SlitHeight, ArchStyle.Flat ),
0.0f, SurfaceMaterial, TrimMaterial );
kit.Material = TrimMaterial;
kit.Box( new Vector3( -8.0f, -8.0f, Height ), new Vector3( length + 8.0f, depth + 8.0f, Height + RoofThickness ) );
kit.Box( new Vector3( 0.0f, 0.0f, -8.0f ), new Vector3( length, depth, 0.0f ) );
}
}