Editor/Tool/Subtools/ArchSpanSubtool.cs
using System;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// A span crosses between two piers, so it belongs to the tool that stands them: this is the Pillars subtool's
// third gesture, composed the way ArchFixtureSubtool is composed by the bool tool, and it holds no shelf entry
// of its own. Keeping the form in its own file keeps the two-click bridge out of the placement tool it rides in.
[Title( "Spans" ), Icon( "bridge" ), Group( "09" )]
public sealed class ArchSpanSubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => ArchKind.Span;
public override ArchSurface[] Surfaces => new[] { ArchSurface.PillarCap };
ArchSpanPart draft = ArchSpanMemo.Recall();
ArchSpanEnd taken;
public ArchSpanPart LastPlaced { get; private set; }
public ArchKind? MergedDraftKind => DraftKind;
public string MergedAdvice => Advice();
public ArchRunState MergedRun => Run();
public bool MergedAdjusts => Adjusts;
public void MergedCancel() => CancelRun();
public void MergedHover( Vector2 point ) => DrawHover( point );
// The host does the draft bookkeeping and the commit, exactly as the bool tool does for a fixture: this
// returns what it stood and nothing else, so there is one place a placement is finished.
public ArchSpanPart MergedClick( Vector2 point ) {
LastPlaced = null;
OnClick( point );
return LastPlaced;
}
protected override bool UsesDrag => false;
// Only over a span. The selection outlives the tool that made it, so an unconditional yes put the LAST
// pillar's box dragger on screen the moment this tool was picked - and the gizmo takes the press first, so
// every click meant for a pier went into a widget belonging to something else.
protected override bool Adjusts => Owner.Picked?.Item is ArchSpanPart;
protected override string Title() => "Spans";
protected override string Advice() => taken is null
? "Click a column or a wall to take one end. The nearest column within reach wins, so an eyeballed click still lands on the pier."
: "Click the far column or wall and it bridges them. An end that lands on nothing stays where it was dropped.";
protected override ArchRunState Run() => new( taken is not null, "one end taken" );
protected override void CancelRun() => taken = null;
protected override void DrawHover( Vector2 point ) {
var room = Owner.RoomOnLevel( point );
if ( room is null ) {
base.DrawHover( point );
return;
}
var landing = ArchSpanShape.Anchored( Owner.Plan, room, point );
Pier( room, landing, false );
if ( taken is null ) {
return;
}
Pier( room, taken, true );
Reaching( room, taken, landing );
}
protected override void OnClick( Vector2 point ) {
var room = Owner.RoomOnLevel( point );
if ( room is null ) {
return;
}
var landing = ArchSpanShape.Anchored( Owner.Plan, room, point );
if ( taken is null ) {
taken = landing;
return;
}
Bridge( room, taken, landing );
}
void Bridge( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to ) {
if ( Standing( room, from, to ) is not { } span || !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out _ ) ) {
Log.Info( "Architecture: nothing to bridge there — a span needs two ends apart, each under a pier tall enough to spring off." );
return;
}
span.Id = Owner.Plan.AllocateId();
span.Name = $"Span{room.PierSpans.Count + 1}";
room.PierSpans.Add( span );
Owner.Picked = new ArchSelection { Item = span, Room = room };
LastPlaced = span;
taken = null;
}
// A ring on the pier's HEAD and a stem down to the floor: where the end will spring from and which column it
// grabbed, drawn as a mark on something rather than as a post, which is a pillar ghost by another name.
void Pier( ArchRoom room, ArchSpanEnd end, bool held ) {
var pier = ArchSpanShape.Pier( Owner.Plan, Owner.Kit, room, end );
var lift = Lift( room );
var head = new Vector3( pier.At.x, pier.At.y, pier.Head + lift );
Gizmo.Draw.LineThickness = held ? 4f : 3f;
Gizmo.Draw.Color = held ? ArchGhost.Accent : ArchGhost.Line;
Gizmo.Draw.LineCircle( head, Vector3.Up, held ? 14f : 10f );
Gizmo.Draw.Line( head, head.WithZ( room.BaseHeight + lift ) );
Gizmo.Draw.LineThickness = 2f;
if ( !pier.Standing ) {
ArchGhost.Cursor( pier.At, room.BaseHeight + lift, ArchSpanShape.Grab );
}
Gizmo.Draw.Color = ArchGhost.Line;
}
// The plan is authored flat and the building is poured onto its grade, so a ghost drawn at plan height stands
// a foundation below the span it is previewing.
float Lift( ArchRoom room ) {
var host = Owner.Plan.OwnerOf( room ) ?? Owner.ActiveBuilding();
return host is null ? 0f : ArchAsks.Lift( Owner.Plan, host, Owner.Kit );
}
void Reaching( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to ) {
var lift = Lift( room );
if ( Standing( room, from, to ) is not { } span
|| !ArchSpanShape.Resolve( Owner.Plan, Owner.Kit, room, span, out var run ) ) {
Gizmo.Draw.Line(
new Vector3( from.At.x, from.At.y, room.BaseHeight + lift ),
new Vector3( to.At.x, to.At.y, room.BaseHeight + lift ) );
return;
}
var start = new Vector3( run.From.x, run.From.y, run.Springing + lift );
var end = new Vector3( run.To.x, run.To.y, run.Springing + lift );
var top = run.Top + lift;
Gizmo.Draw.LineThickness = 4f;
Gizmo.Draw.Color = ArchGhost.Accent;
Gizmo.Draw.Line( start, end );
Gizmo.Draw.Line( start.WithZ( top ), end.WithZ( top ) );
Gizmo.Draw.Line( start, start.WithZ( top ) );
Gizmo.Draw.Line( end, end.WithZ( top ) );
Gizmo.Draw.LineThickness = 2f;
Gizmo.Draw.Color = ArchGhost.Line;
ArchGhost.Note( Vector3.Lerp( start.WithZ( top ), end.WithZ( top ), 0.5f ),
$"{span.Form} — {(run.To - run.From).Length:0} across, {run.Top - run.Springing:0} deep, springs at {run.Springing - room.BaseHeight:0}" );
}
// ONE resolution the ghost and the placed part both read, so what the gesture showed is what the mesh comes out as.
ArchSpanPart Standing( ArchRoom room, ArchSpanEnd from, ArchSpanEnd to ) {
if ( room is null ) {
return null;
}
var span = draft.Dressing();
span.Level = room.Floor;
span.From = from;
span.To = to;
return span;
}
// The host lays the rows: a bridge is the Pillars tool's third gesture, so its two questions belong in that
// tool's one sequence rather than in a second list numbered from 1 underneath it.
public void MergedSteps( ArchWorkflow flow ) {
var span = Edited;
var changed = Changed( span );
flow.Step( "Form", span.Form.ToString(), ArchSpanUi.Glyph( span.Form ), step =>
ArchSpanUi.Form( step.Layout, span, step.Chose, changed ) );
flow.Step( "Section", ArchSpanUi.Measured( span ), "straighten", step =>
ArchSpanUi.Section( step.Layout, span, changed ) );
}
// The picked span if there is one, else the seed for the next bridge - the same two steps either way, which is
// what keeps a tuned span and the next one drawn from agreeing.
public ArchSpanPart Edited => Editing() ?? draft;
public string PickedName => Editing()?.Name;
Action Changed( ArchSpanPart span ) {
return Editing() is null
? () => ArchSpanMemo.Remember( draft )
: () => { Seed( span ); Owner.Touch( "Edit Span" ); };
}
// Tuning the span that was just placed IS the author saying how the next one should look - the panel points at
// the selection, so without this every adjustment was spent on one part and the seed never moved.
void Seed( ArchSpanPart span ) {
draft = span.Dressing();
ArchSpanMemo.Remember( draft );
}
ArchSpanPart Editing() => Owner.Picked?.Item as ArchSpanPart;
}