Editor/Tunnel/ArchTunnelShape.cs

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 );
}