Editor partial class for AdvancedMappingTools' RoofEditor. Adds the roof overhang, edge trim profile, gutter and downpipe settings, and builds the overhang bounds plus the edge-trim mesh (square, stepped, drip profiles) for flat, gabled, cross-gable, dome and conical roofs.
namespace AdvancedMappingTools;
public enum RoofEdgeProfile
{
Square,
Stepped,
Drip
}
public sealed partial class RoofEditor
{
[Property, Group( "Roof Overhangs" ), Title( "Separate Four Sides" ), WideMode]
public bool SeparateOverhangs { get; set; }
[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
public float StartOverhang { get; set; } = 8;
[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
public float FinishOverhang { get; set; } = 8;
[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
public float FrontOverhang { get; set; } = 8;
[Property, Group( "Roof Overhangs" ), Range( 0, 256, slider: false ), WideMode]
public float BackOverhang { get; set; } = 8;
[Property, Group( "Roof Edge" ), WideMode]
public bool EdgeTrim { get; set; }
[Property, Group( "Roof Edge" ), WideMode]
public RoofEdgeProfile EdgeProfile { get; set; }
[Property, Group( "Roof Edge" ), Range( .25f, 32, slider: false ), WideMode]
public float EdgeTrimDepth { get; set; } = 4;
[Property, Group( "Roof Edge" ), Title( "Edge Material" ), WideMode]
public Material EdgeTrimSurface { get; set; }
[Property, Group( "Gutters" ), WideMode]
public bool FrontGutter { get; set; } = true;
[Property, Group( "Gutters" ), WideMode]
public bool BackGutter { get; set; } = true;
[Property, Group( "Drainage" ), Title( "Front Downpipes" ), WideMode]
public bool FrontDownpipes { get; set; } = true;
[Property, Group( "Drainage" ), Title( "Back Downpipes" ), WideMode]
public bool BackDownpipes { get; set; } = true;
[Property, Group( "Drainage" ), WideMode]
public bool ManualDownpipePosition { get; set; }
[Property, Group( "Drainage" ), Title( "Downpipe Along Edge (%)" ), Range( 0, 100, slider: false ), WideMode]
public float DownpipeAlong { get; set; } = 50;
void RoofBounds( out float x0, out float x1, out float y0, out float y1 )
{
var ex = EndOverhang < 0 ? Overhang : EndOverhang;
var ey = EaveOverhang < 0 ? Overhang : EaveOverhang;
var m = MathF.Max( WallMargin, 0 );
x0 = -(SeparateOverhangs ? Math.Clamp( StartOverhang, 0, 256 ) : ex) - m;
x1 = RectLength + (SeparateOverhangs ? Math.Clamp( FinishOverhang, 0, 256 ) : ex) + m;
y0 = -(SeparateOverhangs ? Math.Clamp( FrontOverhang, 0, 256 ) : ey) - m;
y1 = RectDepth + (SeparateOverhangs ? Math.Clamp( BackOverhang, 0, 256 ) : ey) + m;
}
void AddEdgeTrim( MeshKit kit, Transform frame, float x0, float x1, float y0, float y1 )
{
if ( !EdgeTrim )
return;
var destination = kit;
kit = new MeshKit
{
Frame = frame,
LiftFlushFaces = false
};
kit.Material = EdgeTrimSurface ?? TrimMaterial;
void FinishTrim()
{
var trim = kit.Finish();
if ( trim == null )
return;
var bodyKit = new MeshKit
{
LiftFlushFaces = false
};
if ( Hollow && Shape != RoofShape.Flat )
BuildHollowRoof( bodyKit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, RoofInteriorSurface ?? TrimMaterial, CustomProfile );
else
BuildRoof( bodyKit, frame, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
var body = bodyKit.Finish();
var clean = MeshCutter.SubtractClean( trim, body, Transform.Zero, EdgeTrimSurface ?? TrimMaterial );
destination.ImportMesh( clean ?? trim, Transform.Zero );
}
var d = Math.Clamp( EdgeTrimDepth, .25f, 32 );
var skin = MathF.Min( d * .25f, 2 );
if ( Shape == RoofShape.CrossGable )
{
var source = new MeshKit
{
LiftFlushFaces = false
};
if ( Hollow )
BuildHollowRoof( source, Transform.Zero, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, ShellThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
else
BuildRoof( source, Transform.Zero, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
var body = source.Finish();
foreach ( var face in body.FaceHandles )
{
var vertices = body.GetFaceVertices( face ).Select( body.GetVertexPosition ).ToArray();
if ( vertices.Length < 3 )
continue;
var normal = Vector3.Zero;
for ( var i = 1; i + 1 < vertices.Length && normal.Length < .001f; i++ )
normal = Vector3.Cross( vertices[i] - vertices[0], vertices[i + 1] - vertices[0] ).Normal;
if ( MathF.Abs( normal.y ) < .7f )
continue;
var center = vertices.Aggregate( Vector3.Zero, ( a, b ) => a + b ) / vertices.Length;
if ( MathF.Min( MathF.Abs( center.y - y0 ), MathF.Abs( center.y - y1 ) ) > MathF.Max( ShellThickness * 2, 2 ) )
continue;
var outward = new Vector3( 0, MathF.Sign( normal.y ), 0 );
var rings = new List<Vector3[]>
{
vertices,
vertices.Select( v => v + outward * skin ).ToArray()
};
if ( EdgeProfile != RoofEdgeProfile.Square )
rings.Add( vertices.Select( v => center + (v - center) * .8f + outward * d + (EdgeProfile == RoofEdgeProfile.Drip ? Vector3.Down * skin * 2 : Vector3.Zero) ).ToArray() );
kit.BeginPart();
kit.Face( rings[0].Reverse().ToArray() );
kit.Face( rings[^1] );
for ( var i = 0; i + 1 < rings.Count; i++ )
for ( var j = 0; j < vertices.Length; j++ )
{
var k = (j + 1) % vertices.Length;
kit.Face( rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j] );
}
}
destination.ImportMesh( kit.Finish(), Transform.Zero );
return;
}
if ( Shape is RoofShape.Dome or RoofShape.Conical )
{
var rx = (x1 - x0) * .5f;
var ry = (y1 - y0) * .5f;
var radial = skin / MathF.Min( rx, ry );
kit.Frame = frame.ToWorld( new Transform( new Vector3( (x0 + x1) * .5f, (y0 + y1) * .5f, 0 ), Rotation.Identity, new Vector3( rx, ry, 1 ) ) );
kit.Ring( Vector3.Zero, 1 + radial, 1, EdgeThickness - d, EdgeThickness, 64 );
if ( EdgeProfile != RoofEdgeProfile.Square )
kit.Ring( Vector3.Zero, 1 + d / MathF.Min( rx, ry ), 1 + radial, EdgeThickness - d, EdgeThickness - d + skin, 64 );
if ( EdgeProfile == RoofEdgeProfile.Drip )
kit.Ring( Vector3.Zero, 1 + d / MathF.Min( rx, ry ), 1 + (d - skin) / MathF.Min( rx, ry ), EdgeThickness - d - skin * 2, EdgeThickness - d, 64 );
kit.Frame = frame;
FinishTrim();
return;
}
var sampleKit = new MeshKit
{
LiftFlushFaces = false
};
BuildRoof( sampleKit, Transform.Zero, x0, x1, y0, y1, Shape, RidgeHeight, EdgeThickness, SurfaceMaterial, TrimMaterial, CustomProfile );
var sample = sampleKit.Finish();
Vector2[] profile = EdgeProfile switch
{
RoofEdgeProfile.Square => [new( 0, -d ), new( skin, -d ), new( skin, 0 ), new( 0, 0 )],
RoofEdgeProfile.Stepped => [new( 0, -d ), new( d, -d ), new( d, -d + skin ), new( skin, -d + skin ), new( skin, 0 ), new( 0, 0 )],
_ => [new( 0, -d ), new( d - skin, -d ), new( d - skin, -d - skin * 2 ), new( d, -d - skin * 2 ), new( d, -d + skin ), new( skin, -d + skin ), new( skin, 0 ), new( 0, 0 )]
};
foreach ( var side in new[]
{
-1,
1
}
)
{
var y = side < 0 ? y0 : y1;
var cross = profile.Select( p => new Vector2( p.x * side, p.y ) ).ToArray();
if ( side < 0 )
Array.Reverse( cross );
var rings = new List<Vector3[]>();
void Finish()
{
if ( rings.Count < 2 )
{
rings.Clear();
return;
}
kit.BeginPart();
kit.Face( rings[0].Reverse().ToArray() );
kit.Face( rings[^1] );
for ( var i = 0; i + 1 < rings.Count; i++ )
for ( var j = 0; j < cross.Length; j++ )
{
var k = (j + 1) % cross.Length;
kit.Face( rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j] );
}
rings.Clear();
}
for ( var i = 0; i <= 32; i++ )
{
var x = x0 + (x1 - x0) * i / 32;
var z = HouseDetailGeometry.Height( sample, new( x, y ) );
if ( !float.IsFinite( z ) )
{
Finish();
continue;
}
rings.Add( cross.Select( p => new Vector3( x, y + p.x, z + p.y ) ).ToArray() );
}
Finish();
}
FinishTrim();
}
}