A readonly struct and a class that describe the geometric and logical shape of an arch tunnel used by editor tooling. ArchTunnelMouth stores per-end metrics (frame, elevation, cover, bank, and wing walls). ArchTunnelShape aggregates parts, frames, inner/outer vertex arrays, carved cells, carve volumes, mouth data, and exposes convenience properties like wing count, usability, bore radius, burial depth, and a Carries(distance) test.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Read at the mouth's own station - a portal in a rising bank stands taller than the far one.
public readonly struct ArchTunnelMouth
{
public ArchFrame Frame { get; init; }
public bool Near { get; init; }
public float Ground { get; init; }
public float Head { get; init; }
public float Foot { get; init; }
// Nothing over the crown means the drag never found a hill - cut and cover.
public float Cover { get; init; }
// The ground at the mouth's own station - it says whether there is a bank for the wings to retain.
public float Bank { get; init; }
// This end's own pair - a wing springs from the headwall's edge, not from the nearest mouth.
public List<ArchWingWall> Wings { get; init; }
}
// One resolve for generator, overlay, terrain bore and arch_measure - what is shown is what is built.
public sealed class ArchTunnelShape
{
public ArchTunnelPart Part { get; init; }
public ArchRoadPart Road { get; init; }
public ArchRoadSpan Span { get; init; }
public List<ArchFrame> Frames { get; init; } = new();
public List<Vector3[]> Inner { get; init; } = new();
public List<Vector3[]> Outer { get; init; } = new();
// Defines what a COLUMN is, and so what a service mouth takes out.
public ArchTunnelSection Section { get; init; }
// The lining cells the cuts standing over this bore take out, by (row, column) of the sweep. Resolved once
// with the rest of the shape, because the generator, the drag ghost and arch_measure all have to agree about
// where the hole is - a cell dropped in one and not the other is a mouth with no chamber behind it.
public HashSet<(int Row, int Column)> Carved { get; init; } = new();
// The cuts themselves, because a bore is not only its lining: the invert under the road and the walkways
// beside it are swept on grids of their own, and a hole the lining gave up with those still standing across
// it is an opening you cannot walk through.
public List<ArchCarveVolume> Cuts { get; init; } = new();
public ArchTunnelMouth Near { get; init; }
public ArchTunnelMouth Far { get; init; }
public int Wings => (Near.Wings?.Count ?? 0) + (Far.Wings?.Count ?? 0);
public bool IsUsable => Frames.Count >= 2 && Section is not null;
// The STRUCTURE, not the drag, runs this station - the bore moved to the hill.
public bool Carries( float distance ) => distance > Span.From && distance < Span.To;
public float Bore => Section is null ? 0f : Section.Half + MathF.Max( 2f, Part.Lining );
public float Buried => MathF.Max( Near.Cover, Far.Cover );
}