Editor/Tool/Subtools/ArchRoadSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Road" ), Icon( "add_road" ), Group( "02" )]
public sealed class ArchRoadSubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => ArchKind.Road;
public override ArchSurface[] Surfaces => new[]
{
ArchSurface.Road, ArchSurface.Pavement, ArchSurface.Kerb, ArchSurface.RoadLine, ArchSurface.Deck, ArchSurface.Railing
};
readonly List<ArchCurveNode> nodes = new();
readonly ArchRoadPart draft = new();
bool straight;
// Nothing else could put a centreline in the air - a deck is the road's own surface.
float lift;
protected override bool UsesDrag => false;
protected override string Title() => "Road";
protected override string Advice() => "Click to lay the centreline. Click the first node again to close a loop.";
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
protected override ArchRunState Run() => new( nodes.Count > 0, $"{nodes.Count} node{(nodes.Count == 1 ? "" : "s")}" );
protected override void FinishRun() => Finish( false );
protected override void CancelRun() => nodes.Clear();
protected override void OnClick( Vector2 point ) {
if ( nodes.Count >= 3 && (Flat( nodes[0] ) - point).Length < Owner.Kit.GridSize ) {
Finish( true );
return;
}
nodes.Add( Node( Grounded( point ) ) );
}
// Tangent mode is per node, so a road can turn a corner and then curve.
ArchCurveNode Node( Vector3 position ) {
return new ArchCurveNode {
Position = position,
Mode = straight ? ArchTangentMode.Linear : ArchTangentMode.Auto
};
}
// Draped on terrain or laid on the level, standing its own section clear of whichever. Neither height lands on
// the ladder: a rung is coarser than the whole ride and rounds it away, which is a road laid in the ground.
Vector3 Grounded( Vector2 point ) {
var lifted = MathF.Max( 0f, lift );
if ( draft.Conform && ArchTerrain.Sample( Owner.Scene, point, out var sampled ) ) {
return Owner.Grid.Draped( new Vector3( point.x, point.y, sampled + draft.Seat( Owner.Kit ) + lifted ) );
}
// A level's height already carries the ground clearance, so only the camber is still owed.
return Owner.Grid.Draped( new Vector3( point.x, point.y, Owner.LevelHeight + MathF.Max( 0f, draft.Camber ) + lifted ) );
}
protected override void DrawHover( Vector2 point ) {
base.DrawHover( point );
if ( nodes.Count == 0 ) {
ArchGhost.Note( new Vector3( point.x, point.y, Owner.LevelHeight ), "click to start the centreline" );
return;
}
var line = new List<ArchCurveNode>( nodes ) { Node( Grounded( point ) ) };
var curve = ArchCurve.Of( line );
if ( !curve.IsUsable ) {
return;
}
var frames = ArchRoadGen.Frames( Sketch( line, false ), curve );
ArchGhost.Curve( curve );
ArchGhost.Verges( curve, frames, draft.HalfWidth );
if ( draft.Pavements != RoadSide.None ) {
ArchGhost.Verges( curve, frames, draft.HalfWidth + draft.PavementWidth );
}
Gizmo.Draw.Color = ArchGhost.Accent;
Gizmo.Draw.LineSphere( nodes[0].Position, Owner.Kit.GridSize * 0.5f );
ArchGhost.Note( frames[^1].Position, $"{curve.Length / 12f:0.#} ft" );
}
ArchRoadPart Sketch( IReadOnlyList<ArchCurveNode> line, bool closed ) {
var road = draft.Copy();
road.Nodes = line.Select( node => node.Copy() ).ToList();
road.Closed = closed;
return road;
}
void Finish( bool closed ) {
if ( nodes.Count >= 2 ) {
var road = Sketch( nodes, closed );
road.Id = Owner.Plan.AllocateId();
road.Name = $"Road{Owner.Plan.Roads().Count + 1}";
Owner.Plan.Units.Add( road );
FinishDraft( road.Id );
Owner.Commit( "Create Road" );
}
nodes.Clear();
}
static Vector2 Flat( ArchCurveNode node ) => new( node.Position.x, node.Position.y );
// One list: the shape of the line, then which sides carry a pavement, a kerb and a barrier - every one of them
// an exclusive answer, so the sequence runs itself down to the typed rows at the end. A barrier contributes its
// own rows straight into this sequence rather than opening a second one.
protected override void BuildOptions( ToolSidebarWidget panel ) {
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Centreline", straight ? "Straight" : "Spline", straight ? "timeline" : "gesture", PickCentreline );
flow.Step( "Pavement", draft.Pavements.ToString(), ArchRoadUi.SideGlyph( draft.Pavements ), step => {
ArchRoadUi.Pavements( step.Layout, draft, step.Chose, null );
ArchRoadUi.PavementSizes( step.Layout, draft, null );
} );
flow.Step( "Kerb", draft.Kerb.ToString(), ArchRoadUi.KerbGlyph( draft.Kerb ), step => {
ArchRoadUi.Kerbs( step.Layout, draft, step.Chose, null );
ArchRoadUi.KerbSizes( step.Layout, draft, null );
} );
flow.Step( "Barrier side", draft.Barriers.ToString(), ArchRoadUi.SideGlyph( draft.Barriers ), step =>
ArchRoadUi.BarrierSide( step.Layout, draft, step.Chose, null ) );
if ( draft.Barriers != RoadSide.None ) {
ArchBarrierUi.Steps( flow, draft.Barrier, Refresh, null );
}
flow.Step( "Carriageway", $"{draft.Lanes} lanes", "road", step => ArchRoadUi.Carriageway( step.Layout, draft, null ) );
flow.Step( "Markings", Marked, "linear_scale", step => ArchRoadUi.Markings( step.Layout, draft, Owner.Kit, null ) );
flow.Step( "Terrain", TerrainName, draft.Conform ? "landscape" : "horizontal_rule", step =>
ArchRoadUi.Terrain( step.Layout, draft, null ) );
flow.Step( "Construction", $"{draft.Camber:0.##} camber", "tune", step =>
ArchRoadUi.Construction( step.Layout, draft, null ) );
}
// Deck height rides with the centreline because it is where the NEXT clicks land, not a property of the section.
void PickCentreline( ArchWorkflowStep step ) {
using ( var shape = ArchIconGrid.In( step.Layout ) ) {
shape.Pick( "Spline — eased through every node", "road_spline", "gesture", !straight,
() => { straight = false; step.Chose(); } );
shape.Pick( "Straight — a corner at every node", "road_straight", "timeline", straight,
() => { straight = true; step.Chose(); } );
}
var deck = ArchPartUi.Number( "Deck height", lift, 0f, value => lift = value );
deck.ToolTip = "Raise it before the middle nodes to carry a run over something, then span it with the Bridge tool.";
step.Layout.Add( deck );
}
string Marked => ArchPartUi.Listed(
(!string.IsNullOrWhiteSpace( draft.CentreLine ), "Centre"),
(!string.IsNullOrWhiteSpace( draft.LaneLine ), "Lane"),
(!string.IsNullOrWhiteSpace( draft.EdgeLine ), "Edge") );
string TerrainName => ArchPartUi.Listed( (draft.Conform, "Follow"), (draft.Stamp, "Stamp") );
}