Editor tool that builds fence geometry along a drawn path. Places posts at path points and spacing intervals, then fills each span with rails, pickets, panels, bars, boards, chain link, concrete or hedge, and can cut in a swinging gate.
namespace AdvancedMappingTools;
public enum FenceStyle
{
Rails,
Pickets,
Panel,
[Title( "Chain Link" )] ChainLink,
[Title( "Metal Bars" )] Bars,
[Title( "Privacy Boards" )] Boards,
[Title( "Concrete Wall" )] Concrete,
Hedge
}
/// <summary>
/// Draw a fence with posts along a path. Placed points always get a post.
/// </summary>
[ToolCategory( "Walls & Floors" )]
[Title( "Fence" ), Icon( "fence" )]
public sealed class FenceEditor( PrimitiveTool tool ) : PathEditor( tool )
{
protected override bool SnapsToLines => true;
protected override int MinPoints => 2;
protected override bool CanClose => true;
protected override IEnumerable<string> Hints => [.. base.Hints, "Click the green point: close loop"];
[Property, WideMode]
public FenceStyle Style { get; set; } = FenceStyle.Rails;
[Property, Range( 4, 1024, slider: false ), WideMode]
public float Height { get; set; } = 48.0f;
[Property, Title( "Post Spacing" ), Range( 8, 1024, slider: false ), WideMode]
public float PostSpacing { get; set; } = 64.0f;
[Property, Title( "Post Size" ), Range( 0.5f, 64, slider: false ), WideMode]
public float PostSize { get; set; } = 4.0f;
[Property, Title( "Rail Count" ), Range( 1, 16, slider: false ), WideMode]
public int RailCount { get; set; } = 2;
[Property, Title( "Rail Size" ), Range( 0.25f, 32, slider: false ), WideMode]
public float RailSize { get; set; } = 2.0f;
[Property, Title( "Picket Spacing" ), Range( 1, 64, slider: false ), WideMode]
public float PicketSpacing { get; set; } = 6.0f;
[Property, Title( "Gate" ), WideMode, Description( "Leaves one span open and builds a gate into it." )]
public bool Gate { get; set; } = false;
[Property, ShowIf( nameof( Gate ), true ), Title( "Gate Width" ), Range( 24, 1024, slider: false ), WideMode]
public float GateWidth { get; set; } = 96.0f;
[Property, ShowIf( nameof( Gate ), true ), Title( "Gate Position" ), Range( 0, 1 ), WideMode, Description( "Where along the fence the gate sits." )]
public float GateAt { get; set; } = 0.5f;
[Property, ShowIf( nameof( Gate ), true ), Title( "Gate Swing" ), Range( 0, 90, slider: false ), WideMode, Description( "How far the two leaves stand open. Zero keeps the gate closed." )]
public float GateSwing { get; set; } = 0.0f;
[Property, ShowIf( nameof( Gate ), true ), Title( "Single Leaf" ), WideMode, Description( "One gate leaf across the whole opening instead of two." )]
public bool GateSingleLeaf { get; set; } = false;
[Property, ShowIf( nameof( GateSingleLeaf ), true ), Title( "Hinge Right" ), WideMode, Description( "Hangs a single leaf on the right gate post instead of the left one." )]
public bool GateHingeRight { get; set; } = false;
[Property, Title( "Post Caps" ), WideMode, Description( "A small cap on top of every fence post." )]
public bool PostCaps { get; set; } = false;
protected override void BuildMesh( MeshKit kit )
{
var posts = CollectPosts();
var spans = Closed ? posts.Count : posts.Count - 1;
var gateSpan = -1;
if ( Gate && spans > 0 )
{
var total = 0.0f;
for ( var i = 0; i < spans; i++ )
total += posts[i].Distance( posts[(i + 1) % posts.Count] );
var target = Math.Clamp( GateAt, 0.0f, 1.0f ) * total;
var walked = 0.0f;
for ( var i = 0; i < spans; i++ )
{
var part = posts[i].Distance( posts[(i + 1) % posts.Count] );
if ( target >= walked && target <= walked + part )
{
gateSpan = i;
break;
}
walked += part;
}
}
for ( var i = 0; i < spans; i++ )
{
var a = posts[i];
var b = posts[(i + 1) % posts.Count];
var direction = (b - a).WithZ( 0.0f );
if ( direction.Length < 0.01f ) continue;
kit.Frame = new Transform( a, Rotation.LookAt( direction.Normal, Vector3.Up ) );
kit.Material = SurfaceMaterial;
BuildPost();
if ( i == spans - 1 && !Closed )
{
kit.Frame = new Transform( b, Rotation.LookAt( direction.Normal, Vector3.Up ) );
BuildPost();
kit.Frame = new Transform( a, Rotation.LookAt( direction.Normal, Vector3.Up ) );
}
kit.Material = TrimMaterial;
if ( i == gateSpan ) BuildGate( direction.Length );
else BuildSpan( direction.Length );
}
void BuildGate( float length )
{
var half = MathF.Min( GateWidth * 0.5f, (length - PostSize) * 0.5f );
if ( half < 8.0f )
{
BuildSpan( length );
return;
}
var rail = RailSize * 0.5f;
var center = length * 0.5f;
var left = center - half;
var right = center + half;
var post = MathF.Max( PostSize * 0.4f, 1.0f );
var bottom = MathF.Max( 4.0f, Height * 0.08f );
var top = Height - MathF.Max( 4.0f, Height * 0.06f );
var bar = MathF.Max( RailSize * 0.35f, 0.5f );
// One leaf between two posts: two rails, two stiles, bars and a brace.
void LeafFrame( float from, float to )
{
if ( to - from < 2.0f ) return;
kit.Box( new Vector3( from, -rail, bottom ), new Vector3( to, rail, bottom + rail * 2.0f ) );
kit.Box( new Vector3( from, -rail, top - rail * 2.0f ), new Vector3( to, rail, top ) );
kit.Box( new Vector3( from, -rail, bottom ), new Vector3( from + rail * 2.0f, rail, top ) );
kit.Box( new Vector3( to - rail * 2.0f, -rail, bottom ), new Vector3( to, rail, top ) );
var gap = MathF.Max( PicketSpacing, 2.0f );
for ( var x = from + rail * 2.0f + gap; x < to - rail * 2.0f - gap * 0.5f; x += gap )
kit.Box( new Vector3( x - bar, -bar, bottom + rail * 2.0f ), new Vector3( x + bar, bar, top - rail * 2.0f ) );
kit.Beam( new Vector3( from + rail * 2.0f, 0.0f, bottom + rail * 2.0f ), new Vector3( to - rail * 2.0f, 0.0f, top - rail * 2.0f ), 1.0f, 1.2f );
}
// The gate posts stand a little higher than the fence.
kit.Box( new Vector3( left - post, -post, 0.0f ), new Vector3( left + post, post, Height + 2.0f ) );
kit.Box( new Vector3( right - post, -post, 0.0f ), new Vector3( right + post, post, Height + 2.0f ) );
// Open leaves hang on their hinge posts and swing out of the fence line.
var swing = Math.Clamp( GateSwing, 0.0f, 90.0f ) * (MathF.PI / 180.0f);
if ( swing > 0.01f )
{
var leaf = GateSingleLeaf ? right - left : half - rail * 2.0f;
var low = bottom + rail * 2.0f;
var high = top - rail * 2.0f;
var mid = (low + high) * 0.5f;
var reach = MathF.Cos( swing ) * leaf;
var swingOut = MathF.Sin( swing ) * leaf;
var leafBar = MathF.Max( RailSize * 0.35f, 0.5f );
var leafGap = MathF.Max( PicketSpacing, 2.0f );
var count = Math.Max( 1, (int)MathF.Floor( (high - low) / leafGap ) );
var hinges = GateSingleLeaf
? (GateHingeRight ? new[] { (right, right - reach) } : new[] { (left, left + reach) })
: new[] { (left, left + reach), (right, right - reach) };
foreach ( var (hinge, end) in hinges )
{
kit.Beam( new Vector3( hinge, 0.0f, mid ), new Vector3( end, -swingOut, mid ), rail * 2.0f, high - low );
kit.Box( new Vector3( hinge - rail, -rail, low ), new Vector3( hinge + rail, rail, high ) );
kit.Box( new Vector3( end - rail, -swingOut - rail, low ), new Vector3( end + rail, -swingOut + rail, high ) );
for ( var i = 1; i < count; i++ )
{
var t = i / (float)count;
var x = hinge + (end - hinge) * t;
var y = -swingOut * t;
kit.Box( new Vector3( x - leafBar, y - leafBar, low ), new Vector3( x + leafBar, y + leafBar, high ) );
}
}
return;
}
if ( GateSingleLeaf )
{
LeafFrame( left, right );
}
else
{
LeafFrame( left, center - 1.0f );
LeafFrame( center + 1.0f, right );
}
}
void BuildPost()
{
var half = PostSize * 0.5f;
kit.Box( new Vector3( -half, -half, 0.0f ), new Vector3( half, half, Height ) );
if ( !PostCaps ) return;
var cap = half * 1.35f;
kit.Box( new Vector3( -cap, -cap, Height ), new Vector3( cap, cap, Height + 1.6f ) );
}
void BuildSpan( float length )
{
var half = PostSize * 0.5f;
var rail = RailSize * 0.5f;
var start = half;
var end = length - half;
if ( end <= start ) return;
if ( Style == FenceStyle.Panel )
{
kit.Box( new Vector3( start, -rail, 0.0f ), new Vector3( end, rail, Height ) );
return;
}
if ( Style == FenceStyle.Concrete )
{
kit.Box( new Vector3( start, -half * 0.6f, 0.0f ), new Vector3( end, half * 0.6f, Height ) );
kit.Box( new Vector3( start, -half, Height ), new Vector3( end, half, Height + 2.0f ) );
return;
}
if ( Style == FenceStyle.Hedge )
{
kit.Box( new Vector3( start, -half * 1.5f, 0.0f ), new Vector3( end, half * 1.5f, Height ) );
return;
}
if ( Style == FenceStyle.ChainLink )
{
// The wire is one thin sheet, its look comes from the material on it.
kit.Box( new Vector3( start, -0.1f, rail ), new Vector3( end, 0.1f, Height - rail * 2.0f ) );
kit.Box( new Vector3( start, -rail, Height - rail * 2.0f ), new Vector3( end, rail, Height ) );
return;
}
if ( Style == FenceStyle.Bars )
{
var gap = MathF.Max( PicketSpacing, 1.0f );
var bars = (int)MathF.Max( 1.0f, MathF.Floor( (end - start) / gap ) );
kit.Box( new Vector3( start, -rail, 2.0f ), new Vector3( end, rail, 2.0f + rail * 2.0f ) );
kit.Box( new Vector3( start, -rail, Height - rail * 2.0f ), new Vector3( end, rail, Height ) );
for ( var b = 0; b < bars; b++ )
{
var x = start + (end - start) * (b + 0.5f) / bars;
kit.Box( new Vector3( x - 0.5f, -0.5f, 2.0f + rail * 2.0f ), new Vector3( x + 0.5f, 0.5f, Height - rail * 2.0f ) );
}
return;
}
if ( Style == FenceStyle.Boards )
{
var width = MathF.Max( PicketSpacing, 1.0f );
var boards = (int)MathF.Max( 1.0f, MathF.Floor( (end - start) / width ) );
var each = (end - start) / boards;
for ( var r = 0; r < 2; r++ )
{
var z = Height * (r + 1.0f) / 3.0f;
kit.Box( new Vector3( start, -rail, z - rail ), new Vector3( end, rail, z + rail ) );
}
for ( var b = 0; b < boards; b++ )
kit.Box( new Vector3( start + b * each + 0.1f, rail, 0.0f ), new Vector3( start + (b + 1) * each - 0.1f, rail + 0.75f, Height ) );
return;
}
var rails = Style == FenceStyle.Pickets ? 2 : Math.Clamp( RailCount, 1, 16 );
for ( var r = 0; r < rails; r++ )
{
var z = Height * (r + 1.0f) / (rails + 1.0f);
kit.Box( new Vector3( start, -rail, z - rail ), new Vector3( end, rail, z + rail ) );
}
if ( Style != FenceStyle.Pickets ) return;
var spacing = MathF.Max( PicketSpacing, 1.0f );
var count = (int)MathF.Max( 1.0f, MathF.Floor( (end - start) / spacing ) );
var picket = spacing * 0.25f;
for ( var p = 0; p < count; p++ )
{
var x = start + (end - start) * (p + 0.5f) / count;
kit.Box( new Vector3( x - picket, rail, 0.0f ), new Vector3( x + picket, rail + 0.5f, Height ) );
}
}
}
/// <summary>
/// Spread posts evenly over each leg of the path so corners keep theirs.
/// </summary>
List<Vector3> CollectPosts()
{
var posts = new List<Vector3>();
void Add( Vector3 post )
{
if ( posts.Count == 0 || !posts[^1].AlmostEqual( post, 0.01f ) )
posts.Add( post );
}
foreach ( var leg in SampleLegs() )
{
var length = 0.0f;
for ( var i = 0; i < leg.Count - 1; i++ )
length += leg[i].Distance( leg[i + 1] );
var spans = (int)MathF.Max( 1.0f, MathF.Round( length / MathF.Max( PostSpacing, 8.0f ) ) );
var step = length / spans;
var next = 0.0f;
var walked = 0.0f;
for ( var i = 0; i < leg.Count - 1; i++ )
{
var part = leg[i].Distance( leg[i + 1] );
while ( part > 0.0f && next <= walked + part && next < length - 0.01f )
{
Add( Vector3.Lerp( leg[i], leg[i + 1], (next - walked) / part ) );
next += step;
}
walked += part;
}
Add( leg[^1] );
}
if ( Closed && posts.Count > 1 && posts[0].AlmostEqual( posts[^1], 0.01f ) )
posts.RemoveAt( posts.Count - 1 );
return posts;
}
}