An editor tool that draws roof geometry. The enum lists about 24 roof shapes (gable, hip, mansard, sawtooth, dome and so on). RectEditor is the base of a three click rectangle tool, and RoofEditor builds the roof mesh with MeshKit: slopes, edge trim, ridge cap, gutters, tile rows and optional dormers that can dock onto finished wall tops. It also has a Custom profile mode.
namespace AdvancedMappingTools;
public enum RoofShape
{
Flat,
Shed,
Gable,
Hip,
Gambrel,
Mansard,
Saltbox,
Butterfly,
Barrel,
[Title( "Cross Gable" )] CrossGable,
Pyramid,
[Title( "Jerkinhead (Clipped Gable)" )] Jerkinhead,
[Title( "Dutch Gable" )] DutchGable,
Monitor,
Sawtooth,
[Title( "M Shape (Double Gable)" )] MShape,
[Title( "Bellcast (Flared Gable)" )] Bellcast,
Clerestory,
Conical,
Dome,
[Title( "Gothic (Pointed Barrel)" )] Gothic,
[Title( "Gull Wing" )] GullWing,
[Title( "Bonnet (Kicked Hip)" )] Bonnet,
[Title( "Custom Profile" )] Custom
}
/// <summary>
/// A rectangle set by three clicks: one edge, then the depth.
/// </summary>
public abstract class RectEditor( PrimitiveTool tool ) : BuilderEditor( tool )
{
protected override int MinPoints => 3;
protected override int MaxPoints => 3;
protected override IEnumerable<string> Hints => ["Points 1 and 2: first edge", "Point 3: depth"];
Vector3 Edge => (Points[1].Position - Points[0].Position).WithZ( 0.0f );
Vector3 Across => new( -Edge.Normal.y, Edge.Normal.x, 0.0f );
float SignedDepth => (Points[2].Position - Points[0].Position).Dot( Across );
protected float RectLength => Edge.Length;
protected float RectDepth => MathF.Abs( SignedDepth );
/// <summary>
/// Frame on the corner of the rectangle, +X along the first edge and +Y across it.
/// </summary>
protected Transform RectFrame => new(
Points[0].Position + Across * MathF.Min( SignedDepth, 0.0f ),
Rotation.LookAt( Edge.Normal, Vector3.Up ) );
protected override bool IsShapeValid => Edge.Length > 1.0f && RectDepth > 1.0f;
}
/// <summary>
/// A roof over a rectangle. The first two points set one edge and its ridge direction, the third the depth.
/// </summary>
[ToolCategory( "Roofs" )]
[Title( "Roof" ), Icon( "roofing" )]
public sealed partial class RoofEditor( PrimitiveTool tool ) : RectEditor( tool )
{
[Property, Group( "Roof" ), WideMode]
public RoofShape Shape { get; set; } = RoofShape.Gable;
[Property, Group( "Roof" ), Title( "Ridge Height" ), Range( 0, 2048, slider: false ), WideMode]
public float RidgeHeight { get; set; } = 48.0f;
[Property, Group( "Roof" ), Title( "Edge Thickness" ), Range( 0.25f, 128, slider: false ), WideMode]
public float EdgeThickness { get; set; } = 4.0f;
[Property, Group( "Roof" ), Range( 0, 256, slider: false ), WideMode]
public float Overhang { get; set; } = 8.0f;
[Property, Group( "Roof" ), Range( -4096, 4096, slider: false ), WideMode, Description( "Raises the roof above the drawn points." )]
public float Elevation { get; set; } = 0.0f;
[Property, ShowIf( nameof( Shape ), RoofShape.Custom ), Group( "Custom Profile" ), Title( "Profile" ), WideMode, Description( "For the Custom Profile shape: points across the roof from the front eave (X 0) to the back eave (X 1), Y from the eave (0) to the ridge height (1)." )]
public List<Vector2> CustomProfile { get; set; } = [new( 0.0f, 0.0f ), new( 0.35f, 0.9f ), new( 0.5f, 1.0f ), new( 0.65f, 0.9f ), new( 1.0f, 0.0f )];
[Property, Group( "Details" ), Title( "Ridge Cap" ), WideMode, Description( "A bar along the top of a gable roof." )]
public bool RidgeCap { get; set; } = false;
[Property, Group( "Details" ), Title( "Tile Rows" ), Range( 0, 64, slider: false ), WideMode, Description( "Raised rows across the slopes of a gable roof, in the trim material." )]
public int TileRows { get; set; } = 0;
[Property, Group( "Details" ), WideMode, Description( "A gutter along both eaves." )]
public bool Gutters { get; set; } = false;
[Property, Advanced, Group( "Roof" ), Title( "Wall Margin" ), Range( 0, 256, slider: false ), WideMode, Description( "Extra width all round. Set to half the wall when the corners snap onto walls, so the roof covers the walls before the overhang starts." )]
public float WallMargin { get; set; } = 0.0f;
protected override bool Connects => true;
protected override IEnumerable<string> Hints => [.. base.Hints, "Click on a wall corner: the roof sits on the walls"];
/// <summary>
/// A point near the top corner of a finished wall lands on it, so the roof sits on the walls.
/// </summary>
protected override Vector3 Dock( Vector3 point )
{
var docked = base.Dock( point );
if ( !ConnectFinished || SceneEditorSession.Active is null ) return docked;
var corner = WallTops.Nearest( SceneEditorSession.Active.Scene, docked, DockRadius + CornerReach );
if ( Points.Count == 0 ) WallMargin = corner?.Half ?? 0.0f;
if ( corner is not { } found ) return docked;
WallMargin = MathF.Max( WallMargin, found.Half );
DrawLabel( found.Top, "On the wall", Color.Green );
return found.Top;
}
const float CornerReach = 16.0f;
protected override Vector3? UnderCursor( Vector3 point ) => WallCornerUnderCursor( point, CornerReach ) ?? base.UnderCursor( point );
[Property, Group( "Dormers" ), Range( 0, 16, slider: false ), WideMode, Description( "Small gabled windows on the front slope of a gable roof." )]
public int Dormers { get; set; } = 0;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Width" ), Range( 8, 256, slider: false ), WideMode]
public float DormerWidth { get; set; } = 32.0f;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Height" ), Range( 8, 256, slider: false ), WideMode]
public float DormerHeight { get; set; } = 24.0f;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Roof" ), WideMode, Description( "Gable, shed, flat, hipped, round barrel or segmental arch." )]
public DormerShape DormerRoof { get; set; } = DormerShape.Gable;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Roof Rise" ), Range( 2, 128, slider: false ), WideMode]
public float DormerRoofRise { get; set; } = 16.0f;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Overhang" ), Range( 0, 32, slider: false ), WideMode]
public float DormerOverhang { get; set; } = 3.0f;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormers On Both Sides" ), WideMode]
public bool DormersBothSides { get; set; }
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window" ), WideMode]
public bool DormerWindow { get; set; } = true;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Width" ), Range( 6, 256, slider: false ), WideMode]
public float DormerWindowWidth { get; set; } = 20.0f;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Height" ), Range( 6, 256, slider: false ), WideMode]
public float DormerWindowHeight { get; set; } = 16.0f;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Corners" ), WideMode]
public FrameCorner DormerWindowCorner { get; set; } = FrameCorner.Mitred;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Window Detail" ), WideMode]
public WindowDetail DormerWindowDetail { get; set; } = WindowDetail.Standard;
[Property, HideIf( nameof( Dormers ), 0 ), Group( "Dormers" ), Title( "Dormer Flashing" ), WideMode]
public bool DormerFlashing { get; set; } = true;
/// <summary>Where the dormers stand on the roof, in its length and depth. Tile rows stop there.</summary>
readonly List<(float X0, float X1, float Y0, float Y1)> _dormerFootprints = [];
/// <summary>
/// One row of tiles along the roof. It stops at roof windows and cut dormers and goes on behind them.
/// </summary>
void TileRow( MeshKit kit, float x0, float x1, float y, float z )
{
var from = x0;
foreach ( var hole in _roofOpenings.Concat( _dormerFootprints ).Where( h => y > h.Y0 - 1.0f && y < h.Y1 + 1.0f && h.X1 > x0 && h.X0 < x1 ).OrderBy( h => h.X0 ) )
{
if ( hole.X0 - from > 1.0f )
kit.Beam( new Vector3( from, y, z ), new Vector3( hole.X0, y, z ), 2.0f, 1.0f );
from = MathF.Max( from, hole.X1 );
}
if ( x1 - from > 1.0f )
kit.Beam( new Vector3( from, y, z ), new Vector3( x1, y, z ), 2.0f, 1.0f );
}
/// <summary>
/// Dormers on the front slope of a gable roof, or on both: each stands on the slope with its
/// own roof ending in the main roof, a window in a real reveal and flashing at its foot.
/// </summary>
void BuildDormers( MeshKit kit, Transform frame, float y0, float y1, float middle )
{
_dormerFootprints.Clear();
if ( !EnableDormers || Dormers <= 0 || (Hollow && CutDormerOpenings) )
return;
var slot = RectLength / Dormers;
var run = MathF.Max( middle - y0, 1.0f );
var tan = RidgeHeight / run;
var pitch = MathF.Atan( tan ) * 180.0f / MathF.PI;
var floor = EdgeThickness + RidgeHeight * 0.4f;
var glass = Material.Load( "materials/glass/glass_a.vmat" );
foreach ( var back in DormersBothSides ? new[] { false, true } : new[] { false } )
{
var foot = back ? y1 - (y1 - middle) * 0.4f : y0 + run * 0.4f;
for ( var i = 0; i < Dormers; i++ )
{
var center = slot * (i + 0.5f);
var overhang = Math.Clamp( DormerOverhang, 0, 32 );
var width = MathF.Min( DormerWidth, slot - 4.0f - overhang * 2.0f );
if ( width < 8.0f )
continue;
var spec = new DormerSpec
{
Shape = DormerRoof,
Width = width,
WallHeight = DormerHeight,
Pitch = pitch,
RoofRise = Math.Clamp( DormerRoofRise, 2, 128 ),
Overhang = overhang,
Window = DormerWindow,
WindowWidth = DormerWindowWidth,
WindowHeight = DormerWindowHeight,
Flashing = DormerFlashing,
BodyMaterial = TrimMaterial,
RoofMaterial = SurfaceMaterial,
TrimMaterial = TrimMaterial,
Frame = new WindowSpec { Width = 2.0f, Depth = 2.5f, Corner = DormerWindowCorner, Detail = DormerWindowDetail, FrameMaterial = TrimMaterial, GlassMaterial = glass, DetailMaterial = TrimMaterial }
};
// Stay below the ridge and its cap.
DormerKit.Fit( spec, RidgeHeight * 0.6f - 3.0f * tan - 3.0f );
kit.Frame = frame.ToWorld( new Transform( new Vector3( center, foot, floor ), back ? Rotation.FromYaw( 180.0f ) : Rotation.Identity ) );
kit.Material = TrimMaterial;
var r = DormerKit.Build( kit, spec );
_dormerFootprints.Add( back
? (center - r.HalfSpan - 1.0f, center + r.HalfSpan + 1.0f, foot - r.Back - 1.0f, foot - r.Front + 1.0f)
: (center - r.HalfSpan - 1.0f, center + r.HalfSpan + 1.0f, foot + r.Front - 1.0f, foot + r.Back + 1.0f) );
}
}
kit.Frame = frame;
}
void BuildRoofBodyMesh( MeshKit kit, int mask = 0 )
{
RefreshWallThickness();
var frame = RectFrame;
frame.Position += Vector3.Up * Elevation;
RoofBounds( out var x0, out var x1, out var y0, out var y1 );
var middle = (y0 + y1) * 0.5f;
var ridge = EdgeThickness + RidgeHeight;
kit.Frame = frame;
if ( mask != 2 )
{
if ( Hollow && Shape != RoofShape.Flat )
{
BuildHollowRoof( kit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, RoofInteriorSurface ?? TrimMaterial, CustomProfile );
if ( BottomClosure )
{
kit.Frame = frame;
kit.Material = TrimMaterial;
kit.Box( new Vector3( -MathF.Max( WallMargin, 0 ), -MathF.Max( WallMargin, 0 ), -Math.Clamp( BottomThickness, .25f, 128 ) ), new Vector3( RectLength + MathF.Max( WallMargin, 0 ), RectDepth + MathF.Max( WallMargin, 0 ), 0 ) );
}
}
else
BuildRoof( kit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
}
if ( mask == 1 )
return;
if ( Hollow && Shape != RoofShape.Flat && FitToWalls )
BuildWallSeat( kit, frame, RectLength, RectDepth, WallMargin > 0 ? WallMargin : 2, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, WallClosureSurface ?? TrimMaterial, CustomProfile, WallClosureThickness );
kit.Frame = frame;
kit.Material = TrimMaterial;
if ( Gutters )
{
var projection = Math.Clamp( GutterProjection, 0, 32 );
var width = MathF.Max( Math.Clamp( GutterWidth, 1, 24 ), Downpipes ? 2.25f * Math.Clamp( DownpipeRadius, .5f, 12 ) + 2 * Math.Clamp( GutterWallThickness, .25f, 4 ) + .5f : 1 );
var height = Math.Clamp( GutterHeight, 1, 24 );
var z = EdgeThickness + Math.Clamp( GutterVerticalOffset, -32, 32 );
AddRoofDrainage( kit, frame, x0 - (GutterEndReturns ? width * .5f : 0), x1 + (GutterEndReturns ? width * .5f : 0), y0 - projection - width * .5f, y1 + projection + width * .5f, z, width, height );
}
AddEdgeTrim( kit, frame, x0, x1, y0, y1 );
if ( Shape != RoofShape.Gable )
return;
if ( RidgeCap )
kit.Beam( new Vector3( x0, middle, ridge ), new Vector3( x1, middle, ridge ), 6.0f, 3.0f );
BuildDormers( kit, frame, y0, y1, middle );
kit.Frame = frame;
kit.Material = TrimMaterial;
for ( var row = 1; row <= TileRows; row++ )
{
var t = row / (TileRows + 1f);
var z = EdgeThickness + RidgeHeight * t + 0.4f;
TileRow( kit, x0, x1, y0 + (middle - y0) * t, z );
TileRow( kit, x0, x1, y1 + (middle - y1) * t, z );
}
kit.Frame = frame;
}
/// <summary>
/// Extrude a cross section, given as (y, z) counter clockwise from the front, along the ridge.
/// </summary>
static void Section( MeshKit kit, Transform frame, IReadOnlyList<Vector2> section, float x0, float x1 )
{
kit.Frame = frame.ToWorld( new Transform( Vector3.Zero, Rotation.FromYaw( 90.0f ) ) );
kit.Profile( section, -x1, -x0 );
kit.Frame = frame;
}
/// <summary>
/// Add a closed roof body. +X runs along the ridge. A hip ridge turns to the longer side.
/// </summary>
/// <summary>
/// A round roof over the ellipse inside the rectangle: the profile runs from the rim
/// (radius share 1) to the top (radius share 0), heights in roof units.
/// </summary>
static void Revolve( MeshKit kit, float x0, float x1, float y0, float y1, IReadOnlyList<Vector2> profile, int sides, Material surface, Material trim )
{
var cx = (x0 + x1) * 0.5f;
var cy = (y0 + y1) * 0.5f;
var rx = (x1 - x0) * 0.5f;
var ry = (y1 - y0) * 0.5f;
Vector3[] Ring( Vector2 p ) => [.. Enumerable.Range( 0, sides ).Select( i =>
{
var angle = MathF.PI * 2.0f * i / sides;
return new Vector3( cx + MathF.Cos( angle ) * rx * p.x, cy + MathF.Sin( angle ) * ry * p.x, p.y );
} )];
kit.BeginPart();
kit.Material = trim;
var rings = profile.Select( Ring ).ToList();
kit.Face( [.. rings[0].Select( v => v.WithZ( 0.0f ) ).Reverse()] );
for ( var i = 0; i < sides; i++ )
{
var j = (i + 1) % sides;
kit.Face( rings[0][i].WithZ( 0.0f ), rings[0][j].WithZ( 0.0f ), rings[0][j], rings[0][i] );
}
kit.Material = surface;
for ( var k = 0; k + 1 < rings.Count; k++ )
{
var top = profile[k + 1].x <= 0.0001f;
for ( var i = 0; i < sides; i++ )
{
var j = (i + 1) % sides;
if ( top ) kit.Face( rings[k][i], rings[k][j], rings[k + 1][0] );
else kit.Face( rings[k][i], rings[k][j], rings[k + 1][j], rings[k + 1][i] );
}
}
}
public static void BuildRoof( MeshKit kit, Transform frame, float x0, float x1, float y0, float y1,
RoofShape shape, float rise, float edge, Material surface, Material trim, IReadOnlyList<Vector2> custom = null, bool flushCross = false )
{
if ( shape == RoofShape.Hip && y1 - y0 > x1 - x0 )
{
kit.Frame = frame.ToWorld( new Transform( Vector3.Zero, Rotation.FromYaw( 90.0f ) ) );
(x0, x1, y0, y1) = (y0, y1, -x1, -x0);
}
var ridge = edge + rise;
var middle = (y0 + y1) * 0.5f;
var width = y1 - y0;
if ( shape == RoofShape.Flat || rise <= 0.0f )
{
kit.Material = surface;
kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );
kit.Frame = frame;
return;
}
switch ( shape )
{
case RoofShape.Gambrel:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.14f, edge + rise * 0.6f ), new( middle, ridge ), new( y0 + width * 0.14f, edge + rise * 0.6f ), new( y0, edge )], x0, x1 );
return;
case RoofShape.Saltbox:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y0 + width * 0.35f, ridge ), new( y0, edge + rise * 0.3f )], x0, x1 );
return;
case RoofShape.Butterfly:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, ridge ), new( middle, edge ), new( y0, ridge )], x0, x1 );
return;
case RoofShape.Barrel:
kit.Material = surface;
var barrel = new List<Vector2> { new( y0, 0 ), new( y1, 0 ) };
for ( var i = 0; i <= 16; i++ )
{
var angle = MathF.PI * i / 16.0f;
barrel.Add( new Vector2( middle + MathF.Cos( angle ) * width * 0.5f, edge + MathF.Sin( angle ) * rise ) );
}
Section( kit, frame, barrel, x0, x1 );
return;
case RoofShape.Mansard:
var inset = MathF.Min( width, x1 - x0 ) * 0.14f;
var kerb = edge + rise * 0.7f;
kit.Material = trim;
kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );
kit.Material = surface;
kit.Hex( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, edge ), new( x0, y1, edge ),
new( x0 + inset, y0 + inset, kerb ), new( x1 - inset, y0 + inset, kerb ), new( x1 - inset, y1 - inset, kerb ), new( x0 + inset, y1 - inset, kerb ) );
var upper = frame.ToWorld( new Transform( Vector3.Up * kerb ) );
kit.Frame = upper;
BuildRoof( kit, upper, x0 + inset, x1 - inset, y0 + inset, y1 - inset, RoofShape.Hip, rise * 0.3f, 0.5f, surface, trim );
kit.Frame = frame;
return;
case RoofShape.Conical:
Revolve( kit, x0, x1, y0, y1, [new( 1.0f, edge ), new( 0.0f, ridge )], 32, surface, trim );
kit.Frame = frame;
return;
case RoofShape.Dome:
var dome = new List<Vector2> { new( 1.0f, edge ) };
for ( var i = 1; i <= 8; i++ )
{
var angle = MathF.PI * 0.5f * i / 8.0f;
dome.Add( new Vector2( i == 8 ? 0.0f : MathF.Cos( angle ), edge + MathF.Sin( angle ) * rise ) );
}
Revolve( kit, x0, x1, y0, y1, dome, 32, surface, trim );
kit.Frame = frame;
return;
case RoofShape.Gothic:
// Two arcs that meet in a point, each with a radius of three quarters of the width.
kit.Material = surface;
var arc = width * 0.75f;
var crown = MathF.Sqrt( arc * arc - (width * 0.5f - arc) * (width * 0.5f - arc) );
var pointed = new List<Vector2> { new( y0, 0 ), new( y1, 0 ) };
for ( var i = 0; i <= 8; i++ )
{
var y = y1 - width * 0.5f * i / 8.0f;
var dy = y - (y1 - arc);
pointed.Add( new Vector2( y, edge + MathF.Sqrt( MathF.Max( arc * arc - dy * dy, 0.0f ) ) / crown * rise ) );
}
for ( var i = 7; i >= 0; i-- )
{
var y = y0 + width * 0.5f * i / 8.0f;
var dy = y - (y0 + arc);
pointed.Add( new Vector2( y, edge + MathF.Sqrt( MathF.Max( arc * arc - dy * dy, 0.0f ) ) / crown * rise ) );
}
Section( kit, frame, pointed, x0, x1 );
return;
case RoofShape.GullWing:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.3f, edge + rise * 0.3f ), new( middle, ridge ), new( y0 + width * 0.3f, edge + rise * 0.3f ), new( y0, edge )], x0, x1 );
return;
case RoofShape.Bonnet:
// A shallow skirt at the eaves under a steep hip, the other way round from a mansard.
var skirt = MathF.Min( width, x1 - x0 ) * 0.15f;
var kick = edge + rise * 0.2f;
kit.Material = trim;
kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );
kit.Material = surface;
kit.Hex( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, edge ), new( x0, y1, edge ),
new( x0 + skirt, y0 + skirt, kick ), new( x1 - skirt, y0 + skirt, kick ), new( x1 - skirt, y1 - skirt, kick ), new( x0 + skirt, y1 - skirt, kick ) );
var bonnet = frame.ToWorld( new Transform( Vector3.Up * kick ) );
kit.Frame = bonnet;
BuildRoof( kit, bonnet, x0 + skirt, x1 - skirt, y0 + skirt, y1 - skirt, RoofShape.Hip, ridge - kick - 0.5f, 0.5f, surface, trim );
kit.Frame = frame;
return;
case RoofShape.Custom:
var given = (custom ?? []).Where( p => p.x >= 0.0f && p.x <= 1.0f ).OrderBy( p => p.x ).ToList();
// Without a usable profile the roof stays a gable rather than nothing.
if ( given.Count < 2 )
{
BuildRoof( kit, frame, x0, x1, y0, y1, RoofShape.Gable, rise, edge, surface, trim );
return;
}
kit.Material = surface;
var outline = new List<Vector2> { new( y0, 0 ), new( y1, 0 ), new( y1, edge ) };
for ( var i = given.Count - 1; i >= 0; i-- )
{
var p = given[i];
if ( (i == given.Count - 1 && p.x >= 0.999f && p.y <= 0.001f) || (i == 0 && p.x <= 0.001f && p.y <= 0.001f) ) continue;
outline.Add( new Vector2( y0 + width * p.x, edge + Math.Clamp( p.y, 0.0f, 1.0f ) * rise ) );
}
outline.Add( new Vector2( y0, edge ) );
Section( kit, frame, outline, x0, x1 );
return;
case RoofShape.Monitor:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.3f, edge + rise * 0.45f ), new( y1 - width * 0.3f, edge + rise * 0.75f ),
new( middle, ridge ), new( y0 + width * 0.3f, edge + rise * 0.75f ), new( y0 + width * 0.3f, edge + rise * 0.45f ), new( y0, edge )], x0, x1 );
return;
case RoofShape.Sawtooth:
kit.Material = surface;
var teeth = Math.Clamp( (int)MathF.Round( width / MathF.Max( rise * 2.0f, 1.0f ) ), 2, 8 );
var tooth = width / teeth;
var saw = new List<Vector2> { new( y0, 0 ), new( y1, 0 ), new( y1, edge ) };
// Each tooth rises from its low edge to a vertical glazed face, the way north lights do.
for ( var k = 0; k < teeth; k++ )
{
saw.Add( new Vector2( y1 - tooth * k, ridge ) );
saw.Add( new Vector2( y1 - tooth * (k + 1), edge ) );
}
Section( kit, frame, saw, x0, x1 );
return;
case RoofShape.MShape:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.25f, ridge ), new( middle, edge + rise * 0.15f ), new( y0 + width * 0.25f, ridge ), new( y0, edge )], x0, x1 );
return;
case RoofShape.Bellcast:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( y1 - width * 0.1f, edge + rise * 0.1f ), new( y1 - width * 0.2f, edge + rise * 0.3f ),
new( middle, ridge ), new( y0 + width * 0.2f, edge + rise * 0.3f ), new( y0 + width * 0.1f, edge + rise * 0.1f ), new( y0, edge )], x0, x1 );
return;
case RoofShape.Clerestory:
kit.Material = surface;
Section( kit, frame, [new( y0, 0 ), new( y1, 0 ), new( y1, edge ), new( middle, edge + rise * 0.5f ), new( middle, ridge ), new( y0, edge + rise * 0.7f )], x0, x1 );
return;
case RoofShape.DutchGable:
// A hip roof up to two thirds, a small gable on top of it.
var step = width * 0.5f * 0.6f;
var level = edge + rise * 0.6f;
kit.Material = trim;
kit.Box( new Vector3( x0, y0, 0.0f ), new Vector3( x1, y1, edge ) );
kit.Material = surface;
kit.Hex( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, edge ), new( x0, y1, edge ),
new( x0 + step, y0 + step, level ), new( x1 - step, y0 + step, level ), new( x1 - step, y1 - step, level ), new( x0 + step, y1 - step, level ) );
var gablet = frame.ToWorld( new Transform( Vector3.Up * level ) );
kit.Frame = gablet;
BuildRoof( kit, gablet, x0 + step, x1 - step, y0 + step, y1 - step, RoofShape.Gable, ridge - level - 0.5f, 0.5f, surface, trim );
kit.Frame = frame;
return;
case RoofShape.CrossGable:
BuildRoof( kit, frame, x0, x1, y0, y1, RoofShape.Gable, rise, edge, surface, trim );
var cross = frame.ToWorld( new Transform( new Vector3( (x0 + x1) * 0.5f, 0.0f, 0.0f ), Rotation.FromYaw( 90.0f ) ) );
var half = (x1 - x0) * 0.2f;
// On its own the cross wing reaches out past the roof; on a house it ends with the eaves.
var reach = (y1 - y0) * 0.5f + (flushCross ? 0.0f : half);
kit.Frame = cross;
BuildRoof( kit, cross, middle - reach, middle + reach, -half, half, RoofShape.Gable, MathF.Min( rise, rise * half * 2.0f / width + rise * 0.5f ), edge, surface, trim );
kit.Frame = frame;
return;
}
kit.BeginPart();
kit.Material = trim;
kit.FacingDown( x0, x1, y0, y1, 0.0f );
switch ( shape )
{
case RoofShape.Shed:
kit.FacingFront( x0, x1, y0, 0.0f, edge );
kit.FacingBack( x0, x1, y1, 0.0f, ridge );
kit.Face( new( x0, y1, 0.0f ), new( x0, y0, 0.0f ), new( x0, y0, edge ), new( x0, y1, ridge ) );
kit.Face( new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x1, y1, ridge ), new( x1, y0, edge ) );
kit.Material = surface;
kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, y1, ridge ), new( x0, y1, ridge ) );
break;
case RoofShape.Gable:
kit.FacingFront( x0, x1, y0, 0.0f, edge );
kit.FacingBack( x0, x1, y1, 0.0f, edge );
kit.Face( new( x0, y1, 0.0f ), new( x0, y0, 0.0f ), new( x0, y0, edge ), new( x0, middle, ridge ), new( x0, y1, edge ) );
kit.Face( new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x1, y1, edge ), new( x1, middle, ridge ), new( x1, y0, edge ) );
kit.Material = surface;
kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, middle, ridge ), new( x0, middle, ridge ) );
kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), new( x0, middle, ridge ), new( x1, middle, ridge ) );
break;
case RoofShape.Pyramid:
var apex = new Vector3( (x0 + x1) * 0.5f, middle, ridge );
kit.FacingFront( x0, x1, y0, 0.0f, edge );
kit.FacingBack( x0, x1, y1, 0.0f, edge );
kit.FacingRearward( x0, y0, y1, 0.0f, edge );
kit.FacingForward( x1, y0, y1, 0.0f, edge );
kit.Material = surface;
kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), apex );
kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), apex );
kit.Face( new( x0, y1, edge ), new( x0, y0, edge ), apex );
kit.Face( new( x1, y0, edge ), new( x1, y1, edge ), apex );
break;
case RoofShape.Jerkinhead:
// A gable whose top is cut back into a small hip at both ends.
var clip = 0.7f;
var clipZ = edge + rise * clip;
var yA = y0 + (middle - y0) * clip;
var yB = y1 - (y1 - middle) * clip;
var cut = middle - yA;
kit.FacingFront( x0, x1, y0, 0.0f, edge );
kit.FacingBack( x0, x1, y1, 0.0f, edge );
kit.Face( new( x0, y1, 0.0f ), new( x0, y0, 0.0f ), new( x0, y0, edge ), new( x0, yA, clipZ ), new( x0, yB, clipZ ), new( x0, y1, edge ) );
kit.Face( new( x1, y0, 0.0f ), new( x1, y1, 0.0f ), new( x1, y1, edge ), new( x1, yB, clipZ ), new( x1, yA, clipZ ), new( x1, y0, edge ) );
kit.Material = surface;
kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1, yA, clipZ ), new( x1 - cut, middle, ridge ), new( x0 + cut, middle, ridge ), new( x0, yA, clipZ ) );
kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), new( x0, yB, clipZ ), new( x0 + cut, middle, ridge ), new( x1 - cut, middle, ridge ), new( x1, yB, clipZ ) );
kit.Face( new( x0, yB, clipZ ), new( x0, yA, clipZ ), new( x0 + cut, middle, ridge ) );
kit.Face( new( x1, yA, clipZ ), new( x1, yB, clipZ ), new( x1 - cut, middle, ridge ) );
break;
case RoofShape.Hip:
var hip = (y1 - y0) * 0.5f;
kit.FacingFront( x0, x1, y0, 0.0f, edge );
kit.FacingBack( x0, x1, y1, 0.0f, edge );
kit.FacingRearward( x0, y0, y1, 0.0f, edge );
kit.FacingForward( x1, y0, y1, 0.0f, edge );
kit.Material = surface;
kit.Face( new( x0, y0, edge ), new( x1, y0, edge ), new( x1 - hip, middle, ridge ), new( x0 + hip, middle, ridge ) );
kit.Face( new( x1, y1, edge ), new( x0, y1, edge ), new( x0 + hip, middle, ridge ), new( x1 - hip, middle, ridge ) );
kit.Face( new( x0, y1, edge ), new( x0, y0, edge ), new( x0 + hip, middle, ridge ) );
kit.Face( new( x1, y0, edge ), new( x1, y1, edge ), new( x1 - hip, middle, ridge ) );
break;
}
kit.Frame = frame;
}
}