Editor-side generator for barrier/fence geometry. It builds posts, rails, slats, guardrails and special styles (catenary, precast, bollards) into an ArchMesh using shape, spec and brushes, providing helper math for along/across/lean and corner points.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
public readonly struct ArchBarrierBrushes
{
public ArchBrush Post { get; init; }
public ArchBrush Panel { get; init; }
public ArchBrush Rail { get; init; }
public ArchBrush Plinth { get; init; }
public ArchBrush Wire { get; init; }
public static ArchBarrierBrushes Of( ArchStyle style, params ArchPalette[] chain )
{
return new ArchBarrierBrushes
{
Post = style.Brush( ArchSurface.Post, chain ),
Panel = style.Brush( ArchSurface.Panel, chain ),
Rail = style.Brush( ArchSurface.Railing, chain ),
Plinth = style.Brush( ArchSurface.Foundation, chain ),
Wire = style.Brush( ArchSurface.Wire, chain )
};
}
}
// Posts and ground rule shared; only infill differs, so fence and barrier never drift.
public static class ArchBarrierGen
{
// A post ending exactly on grade floats the moment the terrain moves.
const float Embed = 5f;
// Both resolve the whole curve and skip; a sub-curve would meet half a panel out.
public static void Build( ArchMesh canvas, ArchBarrierShape shape, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes, Func<float, bool> skip = null )
{
if ( shape is null || !shape.IsUsable || spec is null )
{
return;
}
// A retaining wall is not a barrier at all - it holds fill, so only ArchFenceGen's adapter stands one.
if ( spec.Style == BarrierStyle.Retaining )
{
return;
}
using ( canvas.Part( ArchPieces.Posts ) )
{
foreach ( var post in shape.Posts )
{
if ( skip?.Invoke( post.Frame.Distance ) == true )
{
continue;
}
Post( canvas, post, spec, kit, brushes );
}
}
using ( canvas.Part( spec.Style == BarrierStyle.Catenary ? ArchPieces.Cables : ArchPieces.Infill ) )
{
foreach ( var bay in shape.Bays )
{
if ( bay.Gate || skip?.Invoke( (bay.From.Frame.Distance + bay.To.Frame.Distance) * 0.5f ) == true )
{
continue;
}
Infill( canvas, bay, spec, kit, brushes );
}
}
}
static void Post( ArchMesh canvas, ArchBarrierPost post, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes )
{
var size = MathF.Max( 2f, spec.PostSize );
var top = post.Seat + spec.Height + MathF.Max( 0f, spec.PostOverhang );
var bottom = post.Foot - Embed;
if ( spec.Style == BarrierStyle.PrecastSlab )
{
ArchBarrierPrecast.Post( canvas, post, spec, kit, brushes, bottom, top );
return;
}
if ( spec.Style == BarrierStyle.Catenary )
{
ArchBarrierCatenary.Pole( canvas, post, spec, brushes, bottom, top );
return;
}
if ( spec.Style == BarrierStyle.Bollards )
{
canvas.Revolve( new Vector3( post.Flat.x, post.Flat.y, bottom ),
new List<Vector2> { new( size * 0.5f, 0f ), new( size * 0.5f, top - bottom - size * 0.4f ), new( size * 0.34f, top - bottom ) },
8, 1f, 1f, brushes.Post );
return;
}
var along = Along( post.Frame );
var lean = Lean( post.Frame, post.Lean );
var half = size * 0.5f;
canvas.Rake( post.Flat - along * half, post.Flat + along * half, -half, half, bottom, bottom, top - 1.4f, top - 1.4f, brushes.Post );
if ( spec.Style is BarrierStyle.Guardrail )
{
return;
}
var flare = half + 0.9f;
var cap = new Vector3( post.Flat.x + lean.x, post.Flat.y + lean.y, top );
canvas.Rake( post.Flat - along * flare, post.Flat + along * flare, -flare, flare, top - 1.4f, top - 1.4f, top, top, brushes.Post );
canvas.Pyramid( cap - new Vector3( flare, flare, 0f ), cap + new Vector3( flare, flare, 0f ), flare * 1.1f, brushes.Post );
}
static void Infill( ArchMesh canvas, ArchBarrierBayShape bay, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes )
{
var inset = MathF.Max( 2f, spec.PostSize ) * 0.5f;
var span = bay.Span;
if ( span < inset * 2f + 2f )
{
return;
}
// Rails lap a bite into the post: on the inset they z-fight the post face.
var lapped = inset - ArchContact.Bite( kit );
switch ( spec.Style )
{
case BarrierStyle.PrecastSlab:
ArchBarrierPrecast.Bay( canvas, bay, spec, kit, brushes, inset );
break;
case BarrierStyle.Catenary:
ArchBarrierCatenary.Strands( canvas, bay, spec, kit, brushes );
break;
case BarrierStyle.Guardrail:
Guardrail( canvas, bay, spec, brushes, lapped );
break;
case BarrierStyle.PostAndRail:
Rails( canvas, bay, spec, MathF.Max( 1f, kit.FenceRailDepth ), MathF.Max( 1f, kit.FenceRailHeight ), brushes.Rail, lapped );
break;
case BarrierStyle.Palisade:
Rails( canvas, bay, spec, MathF.Max( 1f, kit.FenceRailDepth ), MathF.Max( 1f, kit.FenceRailHeight ), brushes.Rail, lapped );
Slats( canvas, bay, spec, kit.PalisadeWidth, kit.PalisadeGap, kit.PalisadeThickness, PicketTop.Pointed, brushes.Rail, inset );
break;
default:
Rails( canvas, bay, spec, MathF.Max( 1f, kit.FenceRailDepth ), MathF.Max( 1f, kit.FenceRailHeight ), brushes.Rail, lapped );
Slats( canvas, bay, spec, kit.PicketWidth, kit.PicketGap, kit.PicketThickness, spec.Top, brushes.Rail, inset );
break;
}
}
static void Rails( ArchMesh canvas, ArchBarrierBayShape bay, ArchBarrierSpec spec, float depth, float thickness, ArchBrush brush, float inset )
{
var rails = Math.Clamp( spec.Rails, 1, 4 );
var from = bay.From.Flat + bay.Along * inset;
var to = bay.To.Flat - bay.Along * inset;
var half = depth * 0.5f;
for ( var index = 0; index < rails; index++ )
{
var fraction = rails == 1 ? 0.5f : index / (float)(rails - 1);
var centre = spec.Height * (0.22f + 0.56f * fraction);
canvas.Rake( from, to, -half, half,
bay.SeatFrom + centre - thickness * 0.5f, bay.SeatTo + centre - thickness * 0.5f,
bay.SeatFrom + centre + thickness * 0.5f, bay.SeatTo + centre + thickness * 0.5f, brush );
}
}
// Counted over the span plus one gap: the last board carries no gap after it.
static void Slats( ArchMesh canvas, ArchBarrierBayShape bay, ArchBarrierSpec spec, float width, float gap, float thickness, PicketTop top, ArchBrush brush, float inset )
{
var span = bay.Span - inset * 2f;
if ( span < width )
{
return;
}
var board = MathF.Max( 1f, width );
var clear = MathF.Max( 0.5f, gap );
var count = ArchDivide.AtLeast( span + clear, board + clear ).Count;
var slack = (span - count * board) / MathF.Max( 1, count - 1 );
var half = MathF.Max( 0.4f, thickness ) * 0.5f;
for ( var index = 0; index < count; index++ )
{
var offset = inset + (count == 1 ? (span - board) * 0.5f : index * (board + slack));
var fraction = offset / MathF.Max( 1f, bay.Span );
var seat = bay.SeatAt( fraction );
// Down only, like the slab wall: a weathered board is short, never tall.
var wobble = -ArchBarrierShape.Noise( spec.Seed, bay.Index * 97 + index, 71 ) * spec.Wear * 1.6f;
Slat( canvas, bay, offset, board, half, seat + 1.5f, seat + spec.Height + wobble, top, brush );
}
}
static void Slat( ArchMesh canvas, ArchBarrierBayShape bay, float offset, float width, float half, float bottom, float top, PicketTop style, ArchBrush brush )
{
var along = bay.Along;
var across = new Vector2( -along.y, along.x );
var start = bay.From.Flat + along * offset;
var shoulder = style switch
{
PicketTop.Flat => top,
PicketTop.Gothic => top - width * 1.4f,
_ => top - width * 0.8f
};
canvas.Rake( start, start + along * width, -half, half, bottom, bottom, shoulder, shoulder, brush );
if ( style == PicketTop.Flat )
{
return;
}
var a = Corner( start, across, -half, shoulder );
var b = Corner( start + along * width, across, -half, shoulder );
var c = Corner( start + along * width, across, half, shoulder );
var d = Corner( start, across, half, shoulder );
var tipNear = Corner( start + along * (width * 0.5f), across, -half, top );
var tipFar = Corner( start + along * (width * 0.5f), across, half, top );
canvas.Polygon( new[] { a, tipNear, b }, brush );
canvas.Polygon( new[] { c, tipFar, d }, brush );
canvas.Polygon( new[] { a, d, tipFar, tipNear }, brush );
canvas.Polygon( new[] { b, tipNear, tipFar, c }, brush );
}
// A w-beam reads as two bands with a groove between them.
static void Guardrail( ArchMesh canvas, ArchBarrierBayShape bay, ArchBarrierSpec spec, ArchBarrierBrushes brushes, float inset )
{
var from = bay.From.Flat + bay.Along * inset;
var to = bay.To.Flat - bay.Along * inset;
var centre = spec.Height * 0.72f;
var band = MathF.Max( 3f, spec.Height * 0.16f );
var groove = band * 0.35f;
foreach ( var lift in new[] { centre + band * 0.5f + groove * 0.5f, centre - band * 0.5f - groove * 0.5f } )
{
canvas.Rake( from, to, 0.6f, 2.4f,
bay.SeatFrom + lift - band * 0.5f, bay.SeatTo + lift - band * 0.5f,
bay.SeatFrom + lift + band * 0.5f, bay.SeatTo + lift + band * 0.5f, brushes.Rail );
}
}
public static Vector2 Along( ArchFrame frame )
{
var along = new Vector2( frame.Along.x, frame.Along.y );
return along.Length < 0.01f ? new Vector2( 1f, 0f ) : along.Normal;
}
public static Vector2 Across( ArchFrame frame )
{
var across = new Vector2( frame.Across.x, frame.Across.y );
return across.Length < 0.01f ? new Vector2( 0f, 1f ) : across.Normal;
}
static Vector2 Lean( ArchFrame frame, float amount ) => Across( frame ) * amount;
static Vector3 Corner( Vector2 flat, Vector2 across, float side, float height )
{
var point = flat + across * side;
return new Vector3( point.x, point.y, height );
}
}