Editor class definitions for road parts used by the architecture editor. Declares ArchRoadPart with properties for geometry, pavement, kerb, markings, barriers, crossings, bridges and tunnels, plus helper methods (Curve, Reach, Back, Seat) and a Copy method; also defines ArchRoadCrossing and enums RoadSide, KerbStyle, CrossingKind.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public enum RoadSide
{
None,
Left,
Right,
Both
}
public enum KerbStyle
{
Square,
Bevelled,
Rolled
}
public enum CrossingKind
{
Driveway,
Footpath,
Zebra
}
// A unit of its own, standing beside the buildings rather than under one.
public sealed class ArchRoadPart : ArchUnit, IArchPainted
{
public ArchRoadPart()
{
Name = "Road";
Kind = ArchKind.Road;
}
public List<ArchCurveNode> Nodes { get; set; } = new();
public bool Closed { get; set; }
public float Width { get; set; } = 288f;
public int Lanes { get; set; } = 2;
public float Thickness { get; set; } = 6f;
public float Camber { get; set; } = 1.5f;
public float Precision { get; set; } = 48f;
public bool Simplify { get; set; } = true;
public float StraightThreshold { get; set; } = 1f;
public int MinimumStraightRun { get; set; } = 3;
public RoadSide Pavements { get; set; } = RoadSide.Both;
public float PavementWidth { get; set; } = 96f;
public float KerbHeight { get; set; } = 6f;
public KerbStyle Kerb { get; set; } = KerbStyle.Bevelled;
public float KerbBevel { get; set; } = 1.5f;
// Without it the kerb is a knife edge with nowhere for its material.
public float KerbWidth { get; set; } = 5f;
// Cheap - adds columns to a section that is swept anyway.
public int PavementCourses { get; set; } = 2;
// Off by default - a pitch rings the kerb face as well as the field.
public float PaverSize { get; set; }
public bool Markings { get; set; } = true;
public string CentreLine { get; set; } = "dashed_white";
public string LaneLine { get; set; } = "";
public string EdgeLine { get; set; } = "";
public RoadSide Barriers { get; set; } = RoadSide.None;
public ArchBarrierSpec Barrier { get; set; } = new() { Style = BarrierStyle.Guardrail };
// From the outer edge of whatever it stands on - pavement, else carriageway.
public float BarrierInset { get; set; } = 10f;
public bool Conform { get; set; } = true;
// Draping and carving are separate answers - a bridge is laid level THROUGH terrain.
public bool Stamp { get; set; } = true;
public List<ArchRoadCrossing> Crossings { get; set; } = new();
public List<ArchBridgePart> Bridges { get; set; } = new();
public List<ArchTunnelPart> Tunnels { get; set; } = new();
// A road is nothing without a centreline; a single dropped node leaves no run to build.
public override bool HasContent => Nodes.Count >= 2;
public ArchCurve Curve() => ArchCurve.Of( Nodes, Closed );
public float HalfWidth => MathF.Max( 24f, Width ) * 0.5f;
public bool Carries( RoadSide setting, bool right )
{
return setting == RoadSide.Both || setting == (right ? RoadSide.Right : RoadSide.Left);
}
// Only the CARRIAGEWAY takes the node's width scale - a pavement does not narrow with the road.
public float Reach( bool right, float widthScale = 1f )
{
return HalfWidth * MathF.Max( 0.01f, widthScale )
+ (Carries( Pavements, right ) ? MathF.Max( 0f, PavementWidth ) : 0f);
}
public float Reach() => MathF.Max( Reach( false ), Reach( true ) );
// ONE number the verge, a drive's landing and the ground carve all read: how far the pavement's back edge
// stands above the crown.
public float Back( ArchKit kit )
{
return -MathF.Max( 0f, Camber ) + MathF.Max( 0f, KerbHeight ) + MathF.Max( 0f, kit?.PavementFall ?? 0f );
}
// Where the crown seats against the grade it is laid on. A road is cut INTO the ground rather than stood on it,
// so wherever a pavement is carried this is negative: the pavement's back edge lands flush with grade and the
// carriageway sits a kerb below it. Without one the carriageway itself is the top, and only the camber is owed.
public float Seat( ArchKit kit )
{
return Pavements == RoadSide.None ? MathF.Max( 0f, Camber ) : -Back( kit );
}
// Listed once here - a field left off one list silently stops applying.
public ArchRoadPart Copy()
{
return new ArchRoadPart
{
Width = Width,
Lanes = Lanes,
Thickness = Thickness,
Camber = Camber,
Precision = Precision,
Simplify = Simplify,
StraightThreshold = StraightThreshold,
MinimumStraightRun = MinimumStraightRun,
Pavements = Pavements,
PavementWidth = PavementWidth,
KerbHeight = KerbHeight,
Kerb = Kerb,
KerbBevel = KerbBevel,
KerbWidth = KerbWidth,
PavementCourses = PavementCourses,
PaverSize = PaverSize,
Markings = Markings,
CentreLine = CentreLine,
LaneLine = LaneLine,
EdgeLine = EdgeLine,
Barriers = Barriers,
Barrier = Barrier.Copy(),
BarrierInset = BarrierInset,
Conform = Conform,
Stamp = Stamp
};
}
}
// The pavement is never broken, only lowered.
public sealed class ArchRoadCrossing
{
public int Id { get; set; }
public string Name { get; set; } = "Crossing";
public CrossingKind Kind { get; set; } = CrossingKind.Driveway;
public float Distance { get; set; }
public float Width { get; set; } = 132f;
public RoadSide Side { get; set; } = RoadSide.Right;
// Linked, it follows the driveway - move the garage and the kerb moves.
public int ApproachId { get; set; }
public bool Apron { get; set; } = true;
// Splayed so a car can turn into it off a moving lane.
public float Splay { get; set; } = 36f;
// 0 takes the kit's default.
public int SplaySegments { get; set; }
// Only the station changes when runs are joined.
public ArchRoadCrossing Copy( float distance )
{
return new ArchRoadCrossing
{
Id = Id,
Name = Name,
Kind = Kind,
Distance = distance,
Width = Width,
Side = Side,
ApproachId = ApproachId,
Apron = Apron,
Splay = Splay,
SplaySegments = SplaySegments
};
}
}