Editor utility for generating 'arch wing' geometry. It computes wing extents and headings relative to an ArchFrame, probes terrain height to find where a wing "dies" into ground, yields ArchWingWall records, and provides a Build helper that submits geometry to an ArchMesh.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Both ends carry their own ground height, so a wing dies into a slope.
public readonly struct ArchWingWall
{
public Vector2 From { get; init; }
public Vector2 To { get; init; }
public float Head { get; init; }
public float Ground { get; init; }
public float Buried { get; init; }
}
// Where a wing's splay is measured FROM, the difference between the two structures.
public enum WingSplay
{
// Off the structure's own direction, angling out: what an abutment wants.
FromRun,
// Off the face's own normal, angling back: what a portal wants.
FromFace
}
// Abutment and portal share one answer: how far before the bank meets their top.
public static class ArchWing
{
// The one granularity terrain is walked at, so a freestanding wall reads the ground exactly as an abutment does.
public const float ProbeStep = 24f;
// A wing retains FILL: where the ground already meets its head, none is built.
public static IEnumerable<ArchWingWall> Splayed(
ArchFrame frame,
Vector3 away,
float leftEdge,
float rightEdge,
float reach,
float splay,
float head,
float datum,
ArchGround ground,
WingSplay measured = WingSplay.FromRun )
{
if ( reach < 12f )
{
yield break;
}
var spread = Math.Clamp( splay, 0f, 75f ).DegreeToRadian();
foreach ( var right in new[] { false, true } )
{
var start = Flat( frame, right ? rightEdge : -leftEdge );
var yaw = Heading( frame, away, right, spread, measured );
var heading = new Vector2( MathF.Cos( yaw ), MathF.Sin( yaw ) );
var died = Dies( start, heading, reach, head, ground, datum );
if ( died < ProbeStep )
{
continue;
}
var end = start + heading * died;
yield return new ArchWingWall
{
From = start,
To = end,
Head = head,
Ground = MathF.Min( head, ground.Under( end, datum ) ),
Buried = datum
};
}
}
// From-face 'back' follows the structure's direction, so it works at either end of a bore.
static float Heading( ArchFrame frame, Vector3 away, bool right, float spread, WingSplay measured )
{
var along = MathF.Atan2( away.y, away.x );
if ( measured == WingSplay.FromRun )
{
return along + (right ? spread : -spread);
}
var outward = right ? frame.Across : -frame.Across;
var face = MathF.Atan2( outward.y, outward.x );
var turn = MathF.Atan2( MathF.Sin( along - face ), MathF.Cos( along - face ) );
return face + MathF.Sign( turn ) * spread;
}
// Authored length is a MAXIMUM: past the bank's meeting point, a wing lies flat.
public static float Dies( Vector2 start, Vector2 heading, float reach, float head, ArchGround ground, float datum )
{
for ( var walked = ProbeStep; walked <= reach; walked += ProbeStep )
{
if ( ground.Under( start + heading * walked, datum ) >= head )
{
return walked;
}
}
return reach;
}
// Dies just proud of the ground: the end is meant to disappear into the bank.
public static void Build( ArchMesh canvas, IEnumerable<ArchWingWall> wings, float thickness, ArchKit kit, ArchBrush brush )
{
var embedment = ArchGround.Embedment( kit );
var half = MathF.Max( 2f, thickness ) * 0.5f;
foreach ( var wing in wings )
{
var foot = wing.Buried - embedment;
var died = Math.Clamp( wing.Ground + ArchContact.Proud( kit ), foot + 1f, wing.Head );
canvas.Rake( wing.From, wing.To, -half, half, foot, wing.Ground - embedment, wing.Head, died, brush );
}
}
static Vector2 Flat( ArchFrame frame, float across )
{
var point = frame.Side( across );
return new Vector2( point.x, point.y );
}
}