Editor subtool for authoring span bridges in the architecture editor. It handles dragging to join two roads or to place a bridge span along a single road, previews the generated bridge, seeds/retunes bridge types, and commits the created bridge into the plan.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Bridge" ), Icon( "linear_scale" ), Group( "04" )]
public sealed class ArchBridgeSubtool( ArchTool owner ) : ArchSubtool( owner )
{
protected override ArchKind? DraftKind => ArchKind.Bridge;
public override ArchSurface[] Surfaces => new[]
{
ArchSurface.Bridge, ArchSurface.Pier, ArchSurface.Post, ArchSurface.Railing, ArchSurface.Road
};
ArchBridgePart draft = new();
// Re-dresses the newest span in the plan; authoring anything else moves the counter.
int placed;
int stamp;
protected override string Title() => "Span Bridge";
protected override string Advice() =>
"Drag from one road to another to join them and span the gap, with the piers standing down to the terrain and clear of whatever road runs under it. Drag along a single road instead to bridge a stretch of it. The deck IS the road's surface, so its height is the height its nodes are at.";
// Shown in the panel - an untold live edit reads as the tool acting alone.
ArchBridgePart Placed => placed != 0 && Owner.Plan.NextId == stamp ? Owner.Plan.FindBridge( placed ) : null;
ArchBridgePart Edited => Placed ?? draft;
// A bridge IS its structure - a type replaces it whole, nothing per-drag to seed.
void Adopt( ArchBridgeType type )
{
if ( Placed is { } span )
{
ArchRoadUi.Retype( Owner, span, type );
Owner.Touch( "Retype Bridge" );
Refresh();
return;
}
draft = type.Spanning( 0f, 0f );
Refresh();
}
void Seeded()
{
if ( draft.Type.Length > 0 || ArchRoadCatalogs.Bridges().FirstOrDefault() is not { } first )
{
return;
}
draft = first.Spanning( 0f, 0f );
}
protected override void OnDrag( Vector2 from, Vector2 to )
{
Seeded();
if ( !Reached( from, out var near, out _ ) )
{
return;
}
if ( Reached( to, out var far, out _ ) && far != near )
{
Join( near, far );
return;
}
if ( Spanned( from, to, out var road, out var span ) )
{
Add( road, span );
}
}
// Bridging makes the two runs ONE; the near keeps name and section, the far is absorbed.
void Join( ArchRoadPart near, ArchRoadPart far )
{
var join = ArchJoin.Across( near, far, ArchBridge.ShortestSpan );
if ( !join.IsUsable )
{
Log.Info( $"Architecture: {join.Refused}." );
return;
}
join.Apply( Owner.Plan, near, far );
Add( near, join.Span );
}
void Add( ArchRoadPart road, ArchRoadSpan span )
{
var bridge = Edited.Copy( span.From, span.To );
bridge.Id = Owner.Plan.AllocateId();
bridge.Name = $"Bridge{road.Bridges.Count + 1}";
road.Bridges.Add( bridge );
FinishDraft( bridge.Id );
Owner.Commit( "Span Bridge" );
placed = bridge.Id;
stamp = Owner.Plan.NextId;
Refresh();
}
protected override void DrawHover( Vector2 point )
{
base.DrawHover( point );
if ( !Reached( point, out var road, out var frame ) )
{
ArchGhost.Note( new Vector3( point.x, point.y, Owner.LevelHeight ), "drag along a road" );
return;
}
var reach = road.Reach();
Gizmo.Draw.Color = ArchGhost.Accent;
Gizmo.Draw.Line( frame.Side( -reach ), frame.Side( reach ) );
ArchGhost.Note( frame.Position, $"{road.Name} {frame.Distance / 12f:0} ft along" );
}
protected override void DrawPreview()
{
if ( !Reached( DragStart, out var near, out _ ) )
{
base.DrawPreview();
return;
}
if ( Reached( DragCurrent, out var far, out _ ) && far != near )
{
Joining( near, far );
return;
}
if ( !Spanned( DragStart, DragCurrent, out var road, out var span ) )
{
base.DrawPreview();
return;
}
// A sketch - Show would edit the plan from inside a preview.
var alone = road.Copy();
alone.Nodes = road.Nodes;
Show( alone, Owner.Resolved( road.Id, road.Curve ), span, road.Name );
}
// The drag shows the joined run - the easing through the join is in neither centreline.
void Joining( ArchRoadPart near, ArchRoadPart far )
{
var join = ArchJoin.Across( near, far, ArchBridge.ShortestSpan );
if ( !join.IsUsable )
{
ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, Owner.LevelHeight ), join.Refused );
return;
}
var sketch = near.Copy();
sketch.Nodes = join.Nodes;
var curve = ArchCurve.Of( join.Nodes );
Gizmo.Draw.Color = ArchGhost.Line;
ArchGhost.Curve( curve );
Show( sketch, curve, join.Span, $"{near.Name} + {far.Name}" );
}
void Show( ArchRoadPart road, ArchCurve curve, ArchRoadSpan span, string what )
{
var bridge = Edited.Copy( span.From, span.To );
// So the walk keeps a station on each end, as the generator's does.
road.Bridges = new List<ArchBridgePart> { bridge };
var shape = ArchBridge.Resolve( road, bridge, Owner.Plan, curve, ArchRoadGen.Frames( road, curve ), Owner.Kit,
new ArchGround( Owner.Scene ) );
ArchRoadPreviewDraw.Bridge( shape, false );
if ( !shape.IsUsable )
{
ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, Owner.LevelHeight ),
$"{span.Length / 12f:0} ft - too short to span" );
return;
}
ArchGhost.Note( shape.Far.Frame.Position,
$"{what}: {span.Length / 12f:0} ft, {shape.Bents.Count} piers, {shape.Deepest / 12f:0.#} ft over the ground" );
}
// Both ends on ONE road - the drag's own; a too-short span is refused outright.
bool Spanned( Vector2 from, Vector2 to, out ArchRoadPart road, out ArchRoadSpan span )
{
span = default;
if ( !Reached( from, out road, out var start ) || !Owner.Resolved( road.Id, road.Curve ).Nearest( to, out var end, out _ ) )
{
return false;
}
span = new ArchRoadSpan
{
From = MathF.Min( start.Distance, end.Distance ),
To = MathF.Max( start.Distance, end.Distance )
};
return span.Length >= ArchBridge.ShortestSpan;
}
// The click must land on the road's own surface - a garden drag spans nothing.
bool Reached( Vector2 point, out ArchRoadPart road, out ArchFrame frame )
{
road = Owner.RoadAt( point, out frame );
return road is not null;
}
protected override void BuildOptions( ToolSidebarWidget panel )
{
Seeded();
var part = Edited;
var live = Placed;
if ( live is not null )
{
panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {live.Name} live. Span anything else and these go back to seeding the next drag." ) );
}
ArchRoadUi.Types( panel, Owner, part.Type, Adopt );
ArchRoadUi.Bridge( panel, part, ArchRoadCatalogs.Bridge( part.Type ),
Refresh, live is null ? null : () => Owner.Touch( $"Retune {live.Name}" ) );
}
}