Editor/Tunnel/ArchTunnel.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Where a road runs THROUGH the ground, answered here and nowhere else - four consumers would otherwise disagree.
public static class ArchTunnel {
// Shorter, and there is no bore - just two portals standing in each other.
public const float ShortestBore = 120f;
const float ProbeStep = 24f;
// A clamp, not a search - past this the drag was describing something else.
const float Trimmed = 0.4f;
// Ground over the crown before a mouth calls it a hill worth burying in.
const float Cover = 24f;
public static ArchTunnelPart Holding( ArchRoadPart road, float distance ) {
return new ArchRoadAttachmentService( road ).TunnelAt( distance );
}
public static bool Held( ArchRoadPart road, float distance ) => Holding( road, distance ) is not null;
// Both bore ends, and every corner of every cut standing over one - without them a straight tunnel is
// simplified away, and a mouth lands on whatever ribs the sweep already had rather than on its own edges.
public static IEnumerable<float> Stations( ArchRoadPart road, ArchCurve curve = null ) {
foreach ( var station in new ArchRoadAttachmentService( road ).TunnelStations() ) {
yield return station;
}
if ( curve is null ) {
yield break;
}
foreach ( var corner in road.Cuts.Where( cut => cut.HasContent ).SelectMany( cut => cut.Outlines() ).SelectMany( loop => loop ) ) {
if ( curve.Nearest( corner, out var frame, out _ ) ) {
yield return frame.Distance;
}
}
}
// The plan is REQUIRED here, unlike on Resolve: this is the build path, and a bore built without it comes
// out whole with every cut through it silently ignored.
public static List<ArchTunnelShape> Shapes(
ArchRoadPart road,
ArchCurve curve,
IReadOnlyList<ArchFrame> frames,
ArchKit kit,
ArchGround ground,
ArchPlan plan ) {
var shapes = new List<ArchTunnelShape>();
foreach ( var part in ArchLayerGate.Enabled( road.Tunnels ) ) {
var shape = Resolve( road, part, curve, frames, kit, ground, plan );
if ( shape.IsUsable ) {
shapes.Add( shape );
}
}
return shapes;
}
public static ArchTunnelShape Resolve(
ArchRoadPart road,
ArchTunnelPart part,
ArchCurve curve,
IReadOnlyList<ArchFrame> frames,
ArchKit kit,
ArchGround ground,
ArchPlan plan = null ) {
return new ArchTunnelShapeBuilder()
.On( road, part )
.Along( curve )
.WithFrames( frames )
.WithKit( kit )
.Under( ground )
.In( plan )
.Create();
}
internal static ArchTunnelShape ResolveCore(
ArchRoadPart road,
ArchTunnelPart part,
ArchCurve curve,
IReadOnlyList<ArchFrame> frames,
ArchKit kit,
ArchGround ground,
ArchPlan plan = null ) {
var span = Clamped( part, curve.Length );
var empty = new ArchTunnelShape { Part = part, Road = road, Span = span };
if ( span.Length < ShortestBore || frames.Count < 2 ) {
return empty;
}
span = Buried( road, part, curve, span, ground );
var bored = ArchJunction.Clipped( curve, frames, span );
if ( bored.Count < 2 ) {
return empty;
}
var cuts = Cutting( plan, part, kit ).ToList();
var heads = Heads( cuts, bored );
var section = ArchTunnelProfile.Section( road, part, bored[0], heads );
var inner = new List<Vector3[]>();
var outer = new List<Vector3[]>();
foreach ( var frame in bored ) {
var ribs = ArchTunnelProfile.Section( road, part, frame, heads );
inner.Add( ArchTunnelProfile.Ring( frame, ribs.Inner ) );
outer.Add( ArchTunnelProfile.Ring( frame, ribs.Outer ) );
if ( ribs.Half > section.Half ) {
section = ribs;
}
}
var near = Mouth( road, part, bored[0], true, section, kit, ground );
var far = Mouth( road, part, bored[^1], false, section, kit, ground );
near = near with { Wings = Wings( road, part, near, section, ground ).ToList() };
far = far with { Wings = Wings( road, part, far, section, ground ).ToList() };
return new ArchTunnelShape {
Part = part,
Road = road,
Span = span,
Frames = bored,
Inner = inner,
Outer = outer,
Section = section,
Carved = ArchMeshSweep.Carved( inner, cuts ),
Cuts = cuts,
Near = near,
Far = far
};
}
// The cut volumes standing over this bore. Scoped to the tunnel as its own host, so a group bounds what may
// open it exactly as it bounds what may open a slab, and ordered against it, so a cut made before the bore
// was driven leaves it alone.
static IEnumerable<ArchCarveVolume> Cutting( ArchPlan plan, ArchTunnelPart part, ArchKit kit ) {
if ( plan is null ) {
return Array.Empty<ArchCarveVolume>();
}
return ArchCut.Volumes( plan, 0, kit, float.MinValue * 0.5f, float.MaxValue * 0.5f,
part.Id, ArchCutAffects.Lining, part.Id );
}
// Ribs at the lift each cut reaches, off the road's crown, so a mouth comes out square rather than stepped to
// whatever facet the arc happened to land on. The lining's own Split cuts BOTH rings at the same parameter.
static List<float> Heads( IReadOnlyList<ArchCarveVolume> cuts, IReadOnlyList<ArchFrame> bored ) {
var lifts = new List<float>();
foreach ( var volume in cuts ) {
var middle = Middle( volume.Footprint );
lifts.Add( volume.Ceiling.At( middle ) - bored[0].Position.z );
lifts.Add( volume.Floor.At( middle ) - bored[0].Position.z );
}
return lifts;
}
static Vector2 Middle( IReadOnlyList<Vector2> loop ) {
var total = Vector2.Zero;
foreach ( var corner in loop ) {
total += corner;
}
return total / MathF.Max( 1, loop.Count );
}
// The bore starts where the ground is a hill; in the open a portal is a wall across the road.
static ArchRoadSpan Buried( ArchRoadPart road, ArchTunnelPart part, ArchCurve curve, ArchRoadSpan span, ArchGround ground ) {
var reach = span.Length * Trimmed;
var moved = new ArchRoadSpan {
From = span.From + Hill( road, part, curve, span.From, reach, 1f, ground ),
To = span.To - Hill( road, part, curve, span.To, reach, -1f, ground )
};
return moved.Length < ShortestBore ? span : moved;
}
static float Hill( ArchRoadPart road, ArchTunnelPart part, ArchCurve curve, float station, float reach, float inward, ArchGround ground ) {
for ( var walked = 0f; walked <= reach; walked += ProbeStep ) {
if ( Over( road, part, curve, station + walked * inward, ground ) >= Cover ) {
return walked;
}
}
return 0f;
}
// Handed no terrain the datum answers as the crown itself - the authored tunnel stands.
static float Over( ArchRoadPart road, ArchTunnelPart part, ArchCurve curve, float station, ArchGround ground ) {
if ( !curve.Sample( station, out var frame ) ) {
return 0f;
}
var crown = Crown( road, part, frame );
return ground.Under( frame.Flat, crown ) - crown;
}
// The top of the bore at a station, in world height.
public static float Crown( ArchRoadPart road, ArchTunnelPart part, ArchFrame frame ) {
var floor = ArchTunnelProfile.Floor( road );
return frame.Position.z + MathF.Max( floor + 48f, part.Headroom ) + MathF.Max( 2f, part.Lining );
}
// Reads the ground in FRONT, the cutting - its own station is the hillside it is buried in.
static ArchTunnelMouth Mouth( ArchRoadPart road, ArchTunnelPart part, ArchFrame frame, bool near, ArchTunnelSection section, ArchKit kit, ArchGround ground ) {
var crown = frame.Position.z + section.Crown + MathF.Max( 2f, part.Lining );
var head = crown + MathF.Max( 0f, part.PortalRise );
var invert = frame.Position.z + section.Floor - MathF.Max( 1f, part.Invert );
var outward = near ? -frame.Along : frame.Along;
var ahead = new Vector2( outward.x, outward.y );
var lowest = ground.Under( frame.Flat, invert );
foreach ( var step in ArchDivide.AtMost( MathF.Max( 12f, part.PortalDepth ), ProbeStep ).Nodes ) {
lowest = MathF.Min( lowest, ground.Under( frame.Flat + ahead * step, invert ) );
}
var bank = ground.Under( frame.Flat, crown );
return new ArchTunnelMouth {
Frame = frame,
Near = near,
Ground = lowest,
Head = head,
Foot = MathF.Min( invert, lowest ) - ArchGround.Embedment( kit ),
Cover = bank - crown,
Bank = bank
};
}
// Coped to the headwall's top; not built where there is no fill to retain.
static IEnumerable<ArchWingWall> Wings( ArchRoadPart road, ArchTunnelPart part, ArchTunnelMouth mouth, ArchTunnelSection section, ArchGround ground ) {
if ( !part.Portals || mouth.Bank - mouth.Frame.Position.z < 12f ) {
return Array.Empty<ArchWingWall>();
}
var frame = mouth.Frame;
var reach = Reach( part, section );
// Off the FACE, not the run - a portal's wings flare the cutting open.
return ArchWing.Splayed( frame, mouth.Near ? -frame.Along : frame.Along, reach, reach,
MathF.Max( 0f, part.WingWall ), part.WingSplay, mouth.Head, mouth.Ground, ground, WingSplay.FromFace );
}
public static float Reach( ArchTunnelPart part, ArchTunnelSection section ) {
return section.Half + MathF.Max( 2f, part.Lining ) + MathF.Max( 0f, part.PortalReach );
}
// What a walkway seats on - its own kerb where the verge has one, the road where not.
public static float Walkway( ArchRoadPart road, ArchTunnelPart part, bool right ) {
if ( !road.Carries( part.Walkways, right ) || part.WalkwayWidth < 6f ) {
return -ArchTunnelProfile.Floor( road );
}
return -ArchTunnelProfile.Floor( road ) + MathF.Max( 0f, part.WalkwayHeight )
+ (road.Carries( road.Pavements, right ) ? MathF.Max( 0f, road.KerbHeight ) : 0f);
}
public static ArchRoadSpan Clamped( ArchTunnelPart part, float length ) {
var from = Math.Clamp( MathF.Min( part.From, part.To ), 0f, length );
var to = Math.Clamp( MathF.Max( part.From, part.To ), 0f, length );
return new ArchRoadSpan { From = from, To = to };
}
}