Editor-side primitive tool editors for architecture: a shared WallPieceEditor base plus DoorEditor, WindowEditor and ArcadeEditor. Each builds wall geometry with openings, frames, leaves, glass, bars, boards and shutters through MeshKit, and DoorEditor can spawn movable Door leaf objects.
namespace AdvancedMappingTools;
public enum DoorLeaves
{
None,
Single,
Double
}
public enum DoorKind
{
[Title( "Hinged" )] Hinged,
[Title( "Sliding" )] Sliding,
[Title( "Garage" )] Garage,
[Title( "Barn" )] Barn
}
public enum WindowKind
{
[Title( "Standard" )] Standard,
[Title( "Tall" )] Tall,
[Title( "Storefront" )] Storefront,
[Title( "Slit" )] Slit
}
/// <summary>
/// Two points that make up one straight wall piece with arched or square openings.
/// </summary>
public abstract class WallPieceEditor( PrimitiveTool tool ) : BuilderEditor( tool )
{
protected override int MinPoints => 2;
protected override int MaxPoints => 2;
protected Vector3 Start => Points[0].Position;
protected Vector3 End => Points[1].Position;
protected float Length => Start.WithZ( 0.0f ).Distance( End.WithZ( 0.0f ) );
protected Rotation Facing => Rotation.LookAt( (End - Start).WithZ( 0.0f ).Normal, Vector3.Up );
protected override bool IsShapeValid => Length > 1.0f;
[Property, Group( "Wall Piece" ), Title( "Wall Height" ), Range( 1, 4096, slider: false ), WideMode]
public float WallHeight { get; set; } = 128.0f;
[Property, Group( "Wall Piece" ), Range( 0.25f, 512, slider: false ), WideMode]
public float Thickness { get; set; } = 8.0f;
[Property, Group( "Wall Piece" ), Title( "Frame Width" ), Range( 0, 64, slider: false ), WideMode]
public float FrameWidth { get; set; } = 3.0f;
[Property, Group( "Wall Piece" ), Title( "Frame Depth" ), Range( 0, 32, slider: false ), WideMode, Description( "How far the frame stands out of the wall." )]
public float FrameDepth { get; set; } = 1.0f;
[Property, Group( "Arch" ), Title( "Arch" ), WideMode]
public ArchStyle Arch { get; set; } = ArchStyle.Flat;
[Property, Group( "Arch" ), Title( "Arch Segments" ), Range( 2, 64, slider: false ), WideMode]
public int ArchSegments { get; set; } = 12;
protected float ArchRise( float width ) => MeshKit.ArchRise( Arch, width );
/// <summary>
/// The wall height to build with, never lower than a small wall.
/// </summary>
protected float SafeWallHeight => MathF.Max( WallHeight, 16.0f );
/// <summary>
/// A frame can take a fifth of the opening on each side, not more.
/// </summary>
protected float FrameFor( float width ) => MathF.Min( FrameWidth, width * 0.2f );
/// <summary>
/// The wall with its openings and frames. <paramref name="head"/> is the top of the arch.
/// </summary>
protected void BuildWall( MeshKit kit, IReadOnlyList<(float Start, float End)> openings, float sill, float head, bool sillFrame, bool frames = true )
{
var width = openings[0].End - openings[0].Start;
var rise = MathF.Min( ArchRise( width ), head - sill - 1.0f );
var spring = head - rise;
var depth = Thickness * 0.5f + FrameDepth;
var frame = FrameFor( width );
if ( Arch == ArchStyle.Flat || rise <= 0.0f )
kit.WallWithOpenings( Length, Thickness, SafeWallHeight, openings, sill, head );
else
kit.WallWithArches( Length, Thickness, SafeWallHeight, openings, sill, spring, rise, Arch, ArchSegments );
kit.Material = TrimMaterial;
foreach ( var (x0, x1) in openings )
{
if ( !frames )
break;
if ( Arch == ArchStyle.Flat || rise <= 0.0f )
kit.OpeningFrame( x0, x1, sill, head, frame, depth, sillFrame );
else
kit.ArchFrame( x0, x1, sill, spring, rise, Arch, ArchSegments, frame, depth, sillFrame );
}
}
/// <summary>
/// The inside of an opening as a closed outline in the XZ plane.
/// </summary>
protected Vector2[] Clearance( float x0, float x1, float sill, float head, float frame )
{
var width = x1 - x0;
var rise = MathF.Min( ArchRise( width + frame * 2.0f ) - frame, head - sill - 1.0f );
var curve = MeshKit.ArchCurve( x0, x1, head - MathF.Max( rise, 0.0f ), rise, Arch, ArchSegments );
return [new( x0, sill ), new( x1, sill ), .. curve.Reverse()];
}
}
/// <summary>
/// A wall piece with a door opening, frame and door leaves.
/// </summary>
[ToolCategory( "Doors & Windows" )]
[Title( "Door" ), Icon( "door_front" )]
public sealed class DoorEditor( PrimitiveTool tool ) : WallPieceEditor( tool )
{
[Property, Group( "Door" ), Title( "Door Width" ), Range( 4, 1024, slider: false ), WideMode]
public float DoorWidth { get; set; } = 56.0f;
[Property, Group( "Door" ), Title( "Door Height" ), Range( 4, 1024, slider: false ), WideMode]
public float DoorHeight { get; set; } = 96.0f;
[Property, Group( "Door" ), Range( -2048, 2048, slider: false ), WideMode, Description( "Moves the opening away from the middle of the wall." )]
public float Offset { get; set; } = 0.0f;
[Property, Group( "Door" ), Title( "Type" ), WideMode, Description( "Hinged leaves, a sliding leaf, a sectional garage panel or two barn leaves." )]
public DoorKind Kind { get; set; } = DoorKind.Hinged;
[Property, Group( "Door" ), WideMode]
public DoorLeaves Leaves { get; set; } = DoorLeaves.Single;
[Property, Group( "Door" ), Title( "Leaf Thickness" ), Range( 0.25f, 16, slider: false ), WideMode]
public float LeafThickness { get; set; } = 2.0f;
[Property, Group( "Door" ), Title( "Open Angle" ), Range( -180, 180, slider: false ), WideMode]
public float OpenAngle { get; set; } = 0.0f;
[Property, Group( "Door" ), Title( "Movable" ), WideMode, Description( "Makes each leaf its own object with a Door component that opens in the game." )]
public bool Movable { get; set; } = false;
[Property, ShowIf( nameof( Movable ), true ), Group( "Door" ), Title( "Swing Angle" ), Range( -180, 180, slider: false ), WideMode, Description( "How far a movable door opens." )]
public float SwingAngle { get; set; } = 90.0f;
[Property, Group( "Materials" ), Title( "Leaf" ), WideMode, Description( "Material of the door leaves. Empty uses the trim material." )]
public Material LeafMaterial { get; set; }
(float X0, float X1, float Head) Opening()
{
var length = Length;
var width = Math.Clamp( DoorWidth, 8.0f, MathF.Max( 8.0f, length - 2.0f ) );
var head = Math.Clamp( DoorHeight, 8.0f, MathF.Max( SafeWallHeight, 8.0f ) );
var x0 = Math.Clamp( (length - width) * 0.5f + Offset, 1.0f, MathF.Max( 1.0f, length - width - 1.0f ) );
return (x0, x0 + width, head);
}
/// <summary>
/// Each leaf as its hinge in wall space, a turn direction and an outline relative to the hinge.
/// </summary>
List<(Vector3 Hinge, float Turn, Vector2[] Shape)> LeafParts()
{
var parts = new List<(Vector3, float, Vector2[])>();
if ( Leaves == DoorLeaves.None ) return parts;
var (x0, x1, head) = Opening();
var frame = FrameFor( x1 - x0 );
var left = x0 + frame;
var right = x1 - frame;
var outline = Clearance( left, right, 0.0f, head - frame, frame );
if ( Leaves == DoorLeaves.Single )
{
parts.Add( (new Vector3( left, 0.0f, 0.0f ), 1.0f, [.. outline.Select( p => new Vector2( p.x - left, p.y ) )]) );
return parts;
}
var middle = (left + right) * 0.5f;
var leftHalf = outline.Where( p => p.x <= middle + 0.001f ).Select( p => new Vector2( p.x - left, p.y ) ).ToList();
leftHalf.Insert( 1, new Vector2( middle - left, 0.0f ) );
var rightHalf = outline.Where( p => p.x >= middle - 0.001f ).Select( p => new Vector2( p.x - right, p.y ) ).ToList();
rightHalf.Insert( 0, new Vector2( middle - right, 0.0f ) );
parts.Add( (new Vector3( left, 0.0f, 0.0f ), 1.0f, [.. leftHalf]) );
parts.Add( (new Vector3( right, 0.0f, 0.0f ), -1.0f, [.. rightHalf]) );
return parts;
}
void BuildPanel( MeshKit kit, float x0, float x1, float head, float frame )
{
var left = x0 + frame;
var right = x1 - frame;
var width = MathF.Max( right - left, 1.0f );
var outside = Thickness * 0.5f + FrameDepth + LeafThickness * 0.5f;
var y0 = outside - LeafThickness * 0.5f;
var y1 = outside + LeafThickness * 0.5f;
if ( Kind == DoorKind.Garage )
{
kit.Box( new Vector3( left, y0, 0.5f ), new Vector3( right, y1, head - frame ) );
for ( var z = 10.0f; z < head - frame - 4.0f; z += 12.0f )
kit.Box( new Vector3( left - 0.4f, y0 - 0.4f, z ), new Vector3( right + 0.4f, y1 + 0.4f, z + 1.2f ) );
return;
}
if ( Kind == DoorKind.Barn )
{
kit.Box( new Vector3( left - width, y0, 0.0f ), new Vector3( left, y1, head - frame ) );
kit.Box( new Vector3( right, y0, 0.0f ), new Vector3( right + width, y1, head - frame ) );
return;
}
// Sliding: the leaf runs on a rail above the opening; Open Angle is the travel.
var shift = width * 0.5f * Math.Clamp( OpenAngle / 90.0f, -1.0f, 1.0f );
kit.Box( new Vector3( left + shift, y0, 0.0f ), new Vector3( right + shift, y1, head - frame ) );
kit.Box( new Vector3( left - width * 0.6f, y0 - 1.0f, head - frame + 2.0f ), new Vector3( right + width * 0.6f, y1 + 1.0f, head - frame + 6.0f ) );
}
protected override void BuildMesh( MeshKit kit )
{
var (x0, x1, head) = Opening();
var frame = new Transform( Start, Facing );
kit.Frame = frame;
BuildWall( kit, [(x0, x1)], 0.0f, head, false );
if ( Kind != DoorKind.Hinged )
{
kit.Material = LeafMaterial ?? TrimMaterial;
BuildPanel( kit, x0, x1, head, FrameFor( x1 - x0 ) );
return;
}
if ( Movable && !IsPreview ) return;
kit.Material = LeafMaterial ?? TrimMaterial;
foreach ( var (hinge, turn, shape) in LeafParts() )
{
kit.Frame = new Transform( frame.PointToWorld( hinge ), Facing * Rotation.FromYaw( OpenAngle * turn ) );
kit.Profile( shape, -LeafThickness * 0.5f, LeafThickness * 0.5f );
}
}
protected override void OnBuilt( MeshComponent component )
{
if ( !Movable || Kind != DoorKind.Hinged ) return;
var frame = new Transform( Start, Facing );
var doors = new List<Sandbox.Mapping.Door>();
foreach ( var (hinge, turn, shape) in LeafParts() )
{
var kit = new MeshKit { Material = LeafMaterial ?? TrimMaterial };
kit.Profile( shape, -LeafThickness * 0.5f, LeafThickness * 0.5f );
var leaf = new GameObject( component.GameObject, true, "Door Leaf" );
leaf.WorldPosition = frame.PointToWorld( hinge );
leaf.WorldRotation = Facing * Rotation.FromYaw( OpenAngle * turn );
leaf.IsStatic = false;
var mesh = leaf.Components.Create<MeshComponent>( false );
mesh.Mesh = kit.Finish();
mesh.SmoothingAngle = 40.0f;
mesh.Enabled = true;
var door = leaf.Components.Create<Sandbox.Mapping.Door>();
door.TargetAngle = SwingAngle * turn;
doors.Add( door );
}
if ( doors.Count == 2 )
{
doors[0].LinkedDoor = doors[1];
doors[1].LinkedDoor = doors[0];
}
}
}
/// <summary>
/// A wall piece with one or more evenly spaced windows.
/// </summary>
[ToolCategory( "Doors & Windows" )]
[Title( "Window" ), Icon( "window" )]
public sealed class WindowEditor( PrimitiveTool tool ) : WallPieceEditor( tool )
{
[Property, Group( "Window" ), Title( "Type" ), WideMode, Description( "Standard, a tall window, a full-height storefront or a narrow slit." )]
public WindowKind Kind { get; set; } = WindowKind.Standard;
[Property, Group( "Window" ), Range( 1, 32, slider: false ), WideMode]
public int Count { get; set; } = 1;
[Property, Group( "Window" ), Title( "Window Width" ), Range( 4, 1024, slider: false ), WideMode]
public float WindowWidth { get; set; } = 48.0f;
[Property, Group( "Window" ), Title( "Window Height" ), Range( 4, 1024, slider: false ), WideMode]
public float WindowHeight { get; set; } = 48.0f;
[Property, Group( "Window" ), Title( "Sill Height" ), Range( 1, 1024, slider: false ), WideMode]
public float SillHeight { get; set; } = 36.0f;
[Property, Group( "Window" ), Title( "Pane Columns" ), Range( 1, 16, slider: false ), WideMode]
public int PaneColumns { get; set; } = 2;
[Property, Group( "Window" ), Title( "Pane Rows" ), Range( 1, 16, slider: false ), WideMode]
public int PaneRows { get; set; } = 2;
[Property, Group( "Window" ), WideMode, Description( "Adds a thin pane inside each window." )]
public bool Glass { get; set; } = true;
[Property, Group( "Window" ), Title( "Iron Bars" ), Range( 0, 32, slider: false ), WideMode]
public int IronBars { get; set; } = 0;
[Property, Group( "Window" ), Title( "Boarded Up" ), Range( 0, 16, slider: false ), WideMode, Description( "Number of planks nailed across the opening." )]
public int BoardedUp { get; set; } = 0;
[Property, Group( "Window" ), Title( "Shutters" ), WideMode]
public bool Shutters { get; set; } = false;
[Property, Group( "Window" ), Range( 0, 9999, slider: false ), WideMode]
public int Seed { get; set; } = 1;
[Property, ShowIf( nameof( Glass ), true ), Group( "Materials" ), Title( "Glass" ), WideMode, Description( "Material of the pane. Empty uses the trim material." )]
public Material GlassMaterial { get; set; }
[Property, Group( "Window Frame" ), Title( "Profiled Frame" ), WideMode, Description( "A profiled frame with real corner joints, glass seated in it, glazing bars and a sill. Under an arch the frame runs as one ring along it; arches too tall for the window keep the plain frame." )]
public bool ProfiledFrame { get; set; } = true;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Frame" ), Title( "Corners" ), WideMode]
public FrameCorner WindowCorner { get; set; } = FrameCorner.Mitred;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Frame" ), Title( "Profile" ), WideMode]
public FrameProfile WindowProfile { get; set; } = FrameProfile.Chamfered;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Frame" ), Title( "Detail" ), WideMode]
public WindowDetail WindowFrameDetail { get; set; } = WindowDetail.Standard;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Frame" ), Title( "Glazing" ), WideMode]
public Glazing WindowGlazing { get; set; } = Glazing.Double;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Frame" ), Title( "Sill" ), WideMode]
public bool WindowSill { get; set; } = true;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Opening Type" ), WideMode, Description( "How the window opens: fixed, side, top or bottom hinged, hung or sliding sashes, louvres, glass blocks, folding panels, Palladian, or pivoting about its middle. Shaped and arched windows are fixed." )]
public WindowType OpeningType { get; set; } = WindowType.Fixed;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Open" ), Range( 0, 1 ), WideMode, Description( "How far the sashes are open. Windows opening outward stay shut behind iron bars or boards." )]
public float OpenAmount { get; set; }
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Shape" ), WideMode, Description( "Round, half round, triangular or octagonal windows; the wall opening follows the shape." )]
public WindowShape WindowShape { get; set; } = WindowShape.Rectangle;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Transom Light" ), Range( 0, 0.6f ), WideMode, Description( "A fixed light over an opening window, as a share of its height. 0 for none." )]
public float TransomLight { get; set; }
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Storm Sash" ), WideMode, Description( "A thin second window outside; inside for windows that open outward." )]
public bool StormSash { get; set; }
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Hardware" ), WideMode, Description( "Handles, hinges and locks." )]
public bool WindowHardware { get; set; } = true;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Louvres" ), Range( 0, 40, slider: false ), WideMode, Description( "Glass louvres of a jalousie. 0 picks a number from the height." )]
public int Louvres { get; set; }
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Folding Panels" ), Range( 2, 12, slider: false ), WideMode]
public int FoldingPanels { get; set; } = 4;
[Property, ShowIf( nameof( ProfiledFrame ), true ), Group( "Window Type" ), Title( "Glass Block Size" ), Range( 3, 24, slider: false ), WideMode]
public float GlassBlockSize { get; set; } = 8;
/// <summary>Shapes whose wall opening is cut to their outline instead of built square or arched.</summary>
bool CutOutline => WindowShape != WindowShape.Rectangle || OpeningType == WindowType.Palladian;
/// <summary>
/// Windows saved before profiled frames existed keep their plain frame.
/// </summary>
protected override void SettingsApplied( IReadOnlyDictionary<string, string> settings )
{
if ( settings.ContainsKey( nameof( PaneColumns ) ) && !settings.ContainsKey( nameof( ProfiledFrame ) ) )
ProfiledFrame = false;
}
protected override void BuildMesh( MeshKit kit )
{
var length = Length;
var count = Math.Clamp( Count, 1, 32 );
var slot = length / count;
var width = Math.Clamp( WindowWidth, 8.0f, MathF.Max( 8.0f, slot - 2.0f ) );
var wall = SafeWallHeight;
var sill = Kind switch
{
WindowKind.Storefront => 0.5f,
WindowKind.Slit => wall * 0.45f,
WindowKind.Tall => wall * 0.12f,
_ => Math.Clamp( SillHeight, 1.0f, wall - 10.0f )
};
var height = Kind switch
{
WindowKind.Storefront => wall - sill - 2.0f,
WindowKind.Slit => MathF.Min( 18.0f, wall * 0.25f ),
WindowKind.Tall => wall * 0.62f,
_ => WindowHeight
};
var columns = Kind == WindowKind.Storefront ? 3 : PaneColumns;
var rows = Kind is WindowKind.Slit or WindowKind.Storefront ? 1 : PaneRows;
var head = Math.Clamp( sill + height, sill + 8.0f, wall - 1.0f );
var frame = FrameFor( width );
var openings = new List<(float Start, float End)>( count );
for ( var i = 0; i < count; i++ )
{
var center = slot * (i + 0.5f);
openings.Add( (center - width * 0.5f, center + width * 0.5f) );
}
kit.Frame = new Transform( Start, Facing );
// An arch needs straight sides at least as tall as the frame is wide.
var archRise = MathF.Min( ArchRise( width ), head - sill - 1.0f );
var profiled = ProfiledFrame && frame >= 0.5f && (Arch == ArchStyle.Flat || archRise <= 0.0f || archRise <= head - sill - frame * 2.0f - 0.5f);
if ( profiled )
{
// Wall and windows fit each other: none is lifted off the other.
kit.BeginAssembly();
if ( CutOutline )
BuildOutlinedWall( kit, openings, sill, head );
else
BuildWall( kit, openings, sill, head, true, false );
foreach ( var (x0, x1) in openings )
BuildProfiled( kit, x0, x1, sill, head, frame, columns, rows, Arch == ArchStyle.Flat ? 0.0f : archRise );
kit.EndAssembly();
return;
}
BuildWall( kit, openings, sill, head, true );
var random = new Random( Seed );
var top = head - ArchRise( width );
if ( top <= sill ) top = head;
foreach ( var (x0, x1) in openings )
{
var left = x0 + frame;
var right = x1 - frame;
var bottom = sill + frame;
kit.Material = TrimMaterial;
BuildMullions( kit, left, right, bottom, Arch == ArchStyle.Flat ? head - frame : top, frame, columns, rows );
for ( var b = 1; b <= IronBars; b++ )
{
var x = left + (right - left) * b / (IronBars + 1f);
kit.Box( new Vector3( x - 0.5f, Thickness * 0.25f - 0.5f, bottom ), new Vector3( x + 0.5f, Thickness * 0.25f + 0.5f, top ) );
}
for ( var b = 0; b < BoardedUp; b++ )
{
var z = bottom + (top - bottom) * (b + 0.5f) / BoardedUp;
var tilt = ((float)random.NextDouble() - 0.5f) * 8.0f;
var y = -Thickness * 0.5f - FrameDepth - 1.0f;
kit.Beam( new Vector3( x0 - 4.0f, y, z - tilt ), new Vector3( x1 + 4.0f, y, z + tilt ), 1.5f, 5.0f );
}
if ( Shutters )
{
var y = -Thickness * 0.5f - FrameDepth;
var half = width * 0.5f;
kit.Box( new Vector3( x0 - half, y - 1.0f, sill ), new Vector3( x0, y, top ) );
kit.Box( new Vector3( x1, y - 1.0f, sill ), new Vector3( x1 + half, y, top ) );
}
if ( !Glass ) continue;
kit.Material = GlassMaterial ?? TrimMaterial;
kit.Profile( Clearance( left, right, bottom, head - frame, frame ), -0.125f, 0.125f );
}
}
/// <summary>
/// One square opening with a frame from <see cref="WindowKit"/>: it fills the opening through the
/// wall and stands out of it by the frame depth. Iron bars stand in front of the frame, boards
/// in front of them, shutters beside the opening.
/// </summary>
void BuildProfiled( MeshKit kit, float x0, float x1, float sill, float head, float frame, int columns, int rows, float rise )
{
var front = -Thickness * 0.5f - FrameDepth;
kit.Material = TrimMaterial;
WindowUnit.Build( kit, x0, x1, sill, head, front, Unit( frame, columns, rows, rise ) );
kit.Material = TrimMaterial;
if ( WindowSill && sill > 1.6f && WindowShape != WindowShape.Round )
{
kit.BeginPart();
WindowKit.Sill( kit, x0, x1, sill, -Thickness * 0.5f, FrameDepth + 2.5f, 1.5f, 2.0f );
}
BuildExtras( kit, x0, x1, sill, head, frame, front );
}
/// <summary>The window unit for one opening, from the options.</summary>
WindowUnitSpec Unit( float frame, int columns, int rows, float rise )
{
// A window opening outward cannot open behind bars or boards.
var blocked = (IronBars > 0 || BoardedUp > 0) && OpeningType is WindowType.Casement or WindowType.DoubleCasement or WindowType.Awning or WindowType.Accordion or WindowType.PivotHorizontal or WindowType.PivotVertical;
return new WindowUnitSpec
{
Type = OpeningType,
Open = blocked ? 0.0f : Math.Clamp( OpenAmount, 0, 1 ),
Transom = TransomLight,
Storm = StormSash,
Hardware = WindowHardware,
Slats = Louvres,
Panels = FoldingPanels,
BlockSize = GlassBlockSize,
HardwareMaterial = TrimMaterial,
Look = new WindowSpec
{
Width = frame,
Depth = Thickness + FrameDepth * 2.0f,
Edge = MathF.Min( 0.75f, frame * 0.3f ),
Corner = WindowCorner,
Profile = WindowProfile,
Detail = WindowFrameDetail,
Glazing = WindowGlazing,
Glass = Glass,
Columns = Math.Clamp( columns, 1, 16 ),
Rows = Math.Clamp( rows, 1, 16 ),
BarWidth = MathF.Max( frame * 0.5f, 0.5f ),
FrameMaterial = TrimMaterial,
GlassMaterial = GlassMaterial ?? TrimMaterial,
DetailMaterial = TrimMaterial,
Arch = rise > 0.0f && WindowShape == WindowShape.Rectangle ? Arch : ArchStyle.Flat,
ArchRise = rise,
ArchSegments = ArchSegments,
Shape = WindowShape
}
};
}
/// <summary>
/// A wall whose openings are cut to the outline of their windows: round, triangular,
/// octagonal or Palladian.
/// </summary>
void BuildOutlinedWall( MeshKit kit, IReadOnlyList<(float Start, float End)> openings, float sill, float head )
{
var material = kit.Material;
var wall = new MeshKit { Frame = kit.Frame, Material = material, LiftFlushFaces = false };
wall.Box( new Vector3( 0, -Thickness * 0.5f, 0 ), new Vector3( Length, Thickness * 0.5f, SafeWallHeight ) );
var mesh = wall.Finish();
var width = openings[0].End - openings[0].Start;
var unit = Unit( FrameFor( width ), 1, 1, 0 );
// All openings in one cut: cutting them one by one left slivers along the earlier holes.
var cutter = new MeshKit { Frame = kit.Frame, Material = material };
foreach ( var (x0, x1) in openings )
{
var outline = WindowUnit.Outline( x0, x1, sill, head, unit );
WindowKit.Loft( cutter, [.. outline.Select( p => new Vector3( p.x, -Thickness * 0.5f - 1.0f, p.y ) )], [.. outline.Select( p => new Vector3( p.x, Thickness * 0.5f + 1.0f, p.y ) )] );
}
mesh = MeshCutter.SubtractClean( mesh, cutter.Finish(), Transform.Zero, material );
kit.ImportMesh( mesh, Transform.Zero );
}
void BuildExtras( MeshKit kit, float x0, float x1, float sill, float head, float frame, float front )
{
var outsideStorm = StormSash && OpeningType is not (WindowType.Casement or WindowType.DoubleCasement or WindowType.Awning or WindowType.Accordion or WindowType.PivotHorizontal or WindowType.PivotVertical);
var left = x0 + frame;
var right = x1 - frame;
for ( var b = 1; b <= IronBars; b++ )
{
var x = left + (right - left) * b / (IronBars + 1f);
kit.Box( new Vector3( x - 0.5f, front - 1.0f, sill + 0.05f ), new Vector3( x + 0.5f, front, head ) );
}
var random = new Random( Seed );
var width = x1 - x0;
for ( var b = 0; b < BoardedUp; b++ )
{
var z = sill + frame + (head - sill - frame * 2.0f) * (b + 0.5f) / BoardedUp;
var tilt = ((float)random.NextDouble() - 0.5f) * 8.0f;
var y = front - (IronBars > 0 ? 2.0f : 1.0f) - (outsideStorm ? 3.0f : 0.0f);
kit.Beam( new Vector3( x0 - 4.0f, y, z - tilt ), new Vector3( x1 + 4.0f, y, z + tilt ), 1.5f, 5.0f );
}
if ( Shutters )
{
var y = -Thickness * 0.5f;
// Shutters of neighbouring windows must not run into each other.
var slot = Length / Math.Clamp( Count, 1, 32 );
var half = MathF.Max( MathF.Min( width * 0.5f, (slot - width) * 0.5f - 0.05f ), 0.5f );
kit.Box( new Vector3( x0 - half, y - 1.0f, sill ), new Vector3( x0, y, head ) );
kit.Box( new Vector3( x1, y - 1.0f, sill ), new Vector3( x1 + half, y, head ) );
}
}
void BuildMullions( MeshKit kit, float x0, float x1, float z0, float z1, float frame, int columns, int rows )
{
var bar = MathF.Max( frame * 0.25f, 0.25f );
var depth = 0.5f;
columns = Math.Clamp( columns, 1, 16 );
rows = Math.Clamp( rows, 1, 16 );
for ( var c = 1; c < columns; c++ )
{
var x = x0 + (x1 - x0) * c / columns;
kit.Box( new Vector3( x - bar, -depth, z0 ), new Vector3( x + bar, depth, z1 ) );
}
for ( var r = 1; r < rows; r++ )
{
var z = z0 + (z1 - z0) * r / rows;
kit.Box( new Vector3( x0, -depth, z - bar ), new Vector3( x1, depth, z + bar ) );
}
}
}
/// <summary>
/// A row of open arches, for cloisters, viaducts and halls.
/// </summary>
[ToolCategory( "Arches & Vaults" )]
[Title( "Arcade" ), Icon( "account_balance" )]
public sealed class ArcadeEditor( PrimitiveTool tool ) : WallPieceEditor( tool )
{
[Property, Group( "Arcade" ), Range( 1, 32, slider: false ), WideMode]
public int Bays { get; set; } = 4;
[Property, Group( "Arcade" ), Title( "Pier Width" ), Range( 1, 256, slider: false ), WideMode]
public float PierWidth { get; set; } = 16.0f;
[Property, Group( "Arcade" ), Title( "Arch Top" ), Range( 4, 2048, slider: false ), WideMode]
public float ArchTop { get; set; } = 104.0f;
protected override void BuildMesh( MeshKit kit )
{
var bays = Math.Clamp( Bays, 1, 32 );
var slot = Length / bays;
var width = MathF.Max( slot - PierWidth, 8.0f );
var head = MathF.Min( ArchTop, SafeWallHeight - 1.0f );
var openings = new List<(float Start, float End)>( bays );
for ( var i = 0; i < bays; i++ )
{
var center = slot * (i + 0.5f);
openings.Add( (center - width * 0.5f, center + width * 0.5f) );
}
kit.Frame = new Transform( Start, Facing );
BuildWall( kit, openings, 0.0f, head, false );
}
}