Editor/Barriers/ArchWing.cs
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 + ArchLap.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 );
}
}