Editor/Porch/ArchPorchFrame.cs

Editor-side generator for porch frames and railings. It computes bay placement, draws columns, beams, railings and balusters onto an ArchMesh using kit and shape parameters.

File AccessNative Interop
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Bays are the unit - a post starts one, a railing fills one.
public static partial class ArchPorchGen
{
	// The generator and the placement ghost read the same resolve, and the gaps in it are read off the
	// flights actually standing - so a flight dragged along the deck takes its gap with it, and a deleted
	// one closes the railing behind it with nothing to keep in step.
	public static List<ArchBay> Bays( ArchPorchShape shape, ArchPorchPart porch, ArchKit kit )
	{
		var size = MathF.Max( 3f, kit.PorchPostSize );

		return ArchBays.Over( shape.Open, MathF.Max( 24f, porch.PostSpacing ), size * 2f,
			ArchPorchSteps.Gates( porch, shape ) );
	}

	// Anything carrying something overhead stands the same post - a porch beam or a storey.
	public static void Stand( ArchMesh canvas, Vector2 at, ArchKit kit, float bottom, float top, ArchBrush brush )
	{
		var half = MathF.Max( 3f, kit.PorchPostSize ) * 0.5f;
		var flare = half + 1f;

		Column( canvas, at, half, bottom + 3f, top - 3f, brush );
		Column( canvas, at, flare, bottom, bottom + 3f, brush );
		Column( canvas, at, flare, top - 3f, top, brush );
	}

	static void Column( ArchMesh canvas, Vector2 at, float half, float bottom, float top, ArchBrush brush )
	{
		canvas.Box(
			new Vector3( at.x - half, at.y - half, bottom ),
			new Vector3( at.x + half, at.y + half, top ),
			brush );
	}

	// Runs the whole open perimeter so the band's corner rules mitre it.
	static void Beam( ArchMesh canvas, ArchPorchShape shape, ArchKit kit, ArchBrush brush )
	{
		var size = MathF.Max( 3f, kit.PorchPostSize );
		var depth = MathF.Max( 3f, kit.PorchBeamDepth );

		foreach ( var run in shape.Open )
		{
			ArchBandSection.Between( -size, size, shape.Head, shape.Head + depth ).Emit( canvas, run, brush );
		}
	}

	static void Railing( ArchMesh canvas, ArchBay bay, ArchPorchShape shape, ArchKit kit, ArchBrush brush )
	{
		var panel = bay.Narrowed( MathF.Max( 3f, kit.PorchPostSize ) * 0.5f );
		var from = panel.From.Point;
		var to = panel.To.Point;

		if ( panel.Span < 4f )
		{
			return;
		}

		var height = MathF.Max( 18f, kit.PorchRailHeight );
		var bottom = shape.Deck + 2.5f;
		var lowerTop = bottom + 2.2f;
		var upperBottom = shape.Deck + height - 3.4f;
		var upperTop = shape.Deck + height;

		canvas.Beam( from, to, -1.5f, 1.5f, bottom, lowerTop, brush );
		canvas.Beam( from, to, -1.5f, 1.5f, upperBottom, upperTop - 1.4f, brush );
		canvas.Beam( from, to, -2.6f, 2.6f, upperTop - 1.4f, upperTop, brush );

		Balusters( canvas, kit, from, to, lowerTop, upperBottom, brush );
	}

	static void Balusters( ArchMesh canvas, ArchKit kit, Vector2 from, Vector2 to, float bottom, float top, ArchBrush brush )
	{
		var silhouette = kit.FindProfile( "baluster_turned" );

		if ( silhouette is null || silhouette.Points.Count < 2 || top - bottom < 6f )
		{
			return;
		}

		var authored = silhouette.Points[^1].y;

		if ( authored < 1f )
		{
			return;
		}

		// Pitch is a minimum - it must clear the turning's widest part.
		var widest = silhouette.Max.x * 2f * MathF.Max( 0.1f, kit.BalusterScale );
		var pitch = MathF.Max( MathF.Max( 3f, kit.BalusterSpacing ), widest + MathF.Max( 0f, kit.BalusterGap ) );
		var reach = to - from;
		var span = reach.Length;

		if ( span < 0.05f )
		{
			return;
		}

		var along = reach / span;
		var stretch = (top - bottom) / authored;

		foreach ( var centre in ArchDivide.AtLeast( span, pitch ).Centres )
		{
			var at = from + along * centre;

			// Ends butt into rails, so the lathe's caps are never seen.
			canvas.Revolve( new Vector3( at.x, at.y, bottom ), silhouette.Points, kit.BalusterSides, kit.BalusterScale, stretch, brush, caps: false );
		}
	}
}