Editor/Road/ArchRoadPart.cs
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
};
}
}