Editor/Barriers/ArchBarrierGen.cs

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.

File AccessNetworking
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 );
	}
}