Editor/Tunnel/ArchTunnelPortal.cs

Editor utility that builds tunnel portal geometry into an ArchMesh. It computes depths, collars, headwall bands, hoods, coping and wings and emits prisms (blocks/slabs) for the portal surfaces based on tunnel shape, mouth frame and style brush.

File Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// The mouth is a CUT LIST, not a subtraction - each outer-profile segment blocks out to the headwall edge.
public static class ArchTunnelPortal
{
	public static void Build( ArchMesh canvas, ArchTunnelShape shape, ArchTunnelMouth mouth, ArchKit kit, ArchStyle style )
	{
		var part = shape.Part;

		if ( !part.Portals )
		{
			return;
		}

		var brush = style.Brush( ArchSurface.Portal, part.Palette, shape.Road.Palette );
		var bite = ArchContact.Bite( kit );
		var depth = MathF.Max( 12f, part.PortalDepth );
		var reach = ArchTunnel.Reach( part, shape.Section );
		var frame = mouth.Frame;
		var lining = MathF.Max( 2f, part.Lining );
		var crown = frame.Position.z + shape.Section.Crown + lining;
		var invert = frame.Position.z + shape.Section.Floor - MathF.Max( 1f, part.Invert );
		// Bands start this far behind the face - a flat headwall against a curving hillside would show a wedge of daylight.
		var collar = MathF.Max( bite, part.Collar );

		Headwall( canvas, shape, mouth, reach, depth, collar, invert, kit, brush );

		if ( part.PortalRise > 0.5f )
		{
			Slab( canvas, mouth, -reach, reach, -collar, depth, crown - bite, mouth.Head, brush );
		}

		Hood( canvas, shape, mouth, reach, depth, bite, crown, brush );
		Coping( canvas, part, mouth, reach, depth, bite, brush );

		ArchWing.Build( canvas, mouth.Wings, MathF.Max( 8f, depth * 0.35f ), kit, brush );
	}

	static void Headwall( ArchMesh canvas, ArchTunnelShape shape, ArchTunnelMouth mouth, float reach, float depth, float collar, float invert, ArchKit kit, ArchBrush brush )
	{
		var bite = ArchContact.Bite( kit );

		Slab( canvas, mouth, -reach, reach, -collar, depth, mouth.Foot, invert + bite, brush );

		var profile = shape.Section.Outer;
		var datum = mouth.Frame.Position.z;

		for ( var index = 0; index < profile.Count - 1; index++ )
		{
			var below = profile[index];
			var above = profile[index + 1];
			var low = datum + MathF.Min( below.y, above.y );
			var high = datum + MathF.Max( below.y, above.y );

			if ( high - low < 0.5f )
			{
				continue;
			}

			// From the profile's chord to the edge - the inner face IS the mouth; bands lap a bite or z-fight.
			var right = below.x + above.x > 0f;
			var edge = right ? reach : -reach;
			// A bite INTO the lining, not onto it - the collar runs alongside the lining's outer face.
			var into = right ? -bite : bite;
			var lower = (below.y < above.y ? below.x : above.x) + into;
			var upper = (below.y < above.y ? above.x : below.x) + into;

			Block( canvas, mouth, lower, edge, low - bite, upper, edge, high, -collar, depth, brush );
		}
	}

	// Its shadow is what reads as a tunnel from the approach - stands out of the face, not flush.
	static void Hood( ArchMesh canvas, ArchTunnelShape shape, ArchTunnelMouth mouth, float reach, float depth, float bite, float crown, ArchBrush brush )
	{
		var part = shape.Part;
		var stand = MathF.Max( 0f, part.Hood );
		var thickness = MathF.Max( 0f, part.HoodDepth );

		if ( stand < 1f || thickness < 1f )
		{
			return;
		}

		Slab( canvas, mouth, -reach, reach, depth - bite, depth + stand, crown, crown + thickness, brush );
	}

	// The band that gives the headwall's top an edge instead of a plain face.
	static void Coping( ArchMesh canvas, ArchTunnelPart part, ArchTunnelMouth mouth, float reach, float depth, float bite, ArchBrush brush )
	{
		var band = MathF.Max( 0f, part.Coping );

		if ( band < 1f )
		{
			return;
		}

		Slab( canvas, mouth, -reach - band, reach + band, -bite, depth + band, mouth.Head - bite, mouth.Head + band, brush );
	}

	static void Slab( ArchMesh canvas, ArchTunnelMouth mouth, float fromAcross, float toAcross, float near, float far, float bottom, float top, ArchBrush brush )
	{
		Block( canvas, mouth, fromAcross, toAcross, bottom, fromAcross, toAcross, top, near, far, brush );
	}

	// Bottom and top carry their OWN across extents so the inner face rakes with the arch.
	static void Block(
		ArchMesh canvas,
		ArchTunnelMouth mouth,
		float bottomFrom,
		float bottomTo,
		float bottom,
		float topFrom,
		float topTo,
		float top,
		float near,
		float far,
		ArchBrush brush )
	{
		if ( top - bottom < 0.5f || MathF.Abs( bottomTo - bottomFrom ) < 0.5f || far - near < 0.5f )
		{
			return;
		}

		var frame = mouth.Frame;
		// Grown AWAY from the bore, or the far portal runs back through the tunnel it closes.
		var outward = mouth.Near ? -frame.Along : frame.Along;

		var lower = new List<Vector3>
		{
			Point( frame, outward, bottomFrom, near, bottom ),
			Point( frame, outward, bottomTo, near, bottom ),
			Point( frame, outward, bottomTo, far, bottom ),
			Point( frame, outward, bottomFrom, far, bottom )
		};

		var upper = new List<Vector3>
		{
			Point( frame, outward, topFrom, near, top ),
			Point( frame, outward, topTo, near, top ),
			Point( frame, outward, topTo, far, top ),
			Point( frame, outward, topFrom, far, top )
		};

		// Every band of a portal runs ACROSS the road, so that is the direction its courses run in - a headwall on a
		// road at any angle but a right one is otherwise mapped to the world's axes and tiles diagonally.
		canvas.Prism( lower, upper, brush, true, null, frame.Across );
	}

	static Vector3 Point( ArchFrame frame, Vector3 outward, float across, float stand, float height )
	{
		return (frame.Side( across ) + outward * stand).WithZ( height );
	}
}