Editor/Tool/Subtools/ArchPillarSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Pillars" ), Icon( "view_column" ), Group( "09" )]
public sealed class ArchPillarSubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => Bridging ? Spans.MergedDraftKind : ArchKind.Pillar;
public override ArchSurface[] Surfaces => new[] { ArchSurface.Pillar, ArchSurface.PillarCap };
enum PillarGesture {
Single,
Grid,
Span
}
ArchPillarPart draft = new();
ArchSpanSubtool spans;
PillarGesture gesture = PillarGesture.Single;
float maxSpacing;
ArchSpanSubtool Spans => spans ??= new ArchSpanSubtool( Owner );
bool Bridging => gesture == PillarGesture.Span || Owner.Picked?.Item is ArchSpanPart;
protected override bool UsesDrag => gesture == PillarGesture.Grid;
// Single uses flush snap; grid keeps tight snap so the drag isn't eaten
public override ArchSnapReach SnapReach( float authored ) {
return gesture == PillarGesture.Single ? ArchWallSnap.Flush( authored ) : base.SnapReach( authored );
}
protected override bool Adjusts => Bridging ? Spans.MergedAdjusts : Owner.Picked?.Item is ArchPillarPart;
protected override string Title() => Bridging ? "Spans" : "Pillars";
protected override ArchRunState Run() => Bridging ? Spans.MergedRun : default;
protected override void CancelRun() => Spans.MergedCancel();
protected override string Advice() => gesture switch {
PillarGesture.Span => Spans.MergedAdvice,
PillarGesture.Grid => "Drag the extent. Bays divide it evenly, so a shorter drag gives shorter spans, and each column climbs to whatever stands over it.",
_ => "Click to stand one column. It climbs to whatever stands over it, and the widgets on it move, turn and section it afterwards."
};
ArchPillarType Kind => Owner.FindPillarType( draft.Type ) ?? Owner.DefaultPillar();
void Seeded() {
if ( draft.Type.Length > 0 || Owner.DefaultPillar() is not { } first ) {
return;
}
draft = first.Standing( default, 0f, 0f );
maxSpacing = MathF.Max( draft.Spacing.x, draft.Spacing.y );
}
void Adopt( ArchPillarType type, ArchWorkflowStep step ) {
draft = type.Standing( default, 0f, 0f );
maxSpacing = MathF.Max( draft.Spacing.x, draft.Spacing.y );
step.Chose();
}
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
protected override void DrawHover( Vector2 point ) {
if ( gesture == PillarGesture.Span ) {
Spans.MergedHover( point );
return;
}
if ( gesture == PillarGesture.Grid ) {
base.DrawHover( point );
return;
}
Seeded();
var seated = Seated( point );
Ghost( seated, point, Owner.RoomOnLevel( seated ), Lone() );
}
protected override void DrawPreview() {
Seeded();
Ghost( DragStart, DragCurrent, Owner.RoomOnLevel( DragStart ), Bays( DragCurrent - DragStart ) );
}
Vector2 Seated( Vector2 point ) {
return gesture == PillarGesture.Single && Snapped
? ArchPillarSnap.Seat( Owner.Plan, Owner.Kit, Pointer, draft )
: point;
}
void Ghost( Vector2 origin, Vector2 note, ArchRoom room, Grid bays ) {
var standing = Standing( origin, bays, room );
var seated = ArchPillarGen.Seat( standing, Owner.Kit, Owner.Plan );
var height = ArchPillarGen.Height( standing, room, Owner.Kit, Owner.Plan );
var lift = Lift( room );
var half = standing.Seated;
foreach ( var column in ArchPillarGen.Columns( standing, seated ) ) {
var at = new Vector2( column.At.x, column.At.y );
var foot = column.At.z + lift;
ArchGhost.Volume( at - half, at + half, foot, foot + height );
}
ArchGhost.Note( new Vector3( note.x, note.y, seated + lift + height ), Measured( standing ) );
}
float Lift( ArchRoom room ) {
var host = Owner.Plan.OwnerOf( room ) ?? Owner.ActiveBuilding();
return host is null ? 0f : ArchAsks.Lift( Owner.Plan, host, Owner.Kit );
}
string Measured( ArchPillarPart standing ) {
var title = Kind?.Title ?? "pillars";
if ( standing.CountX <= 1 && standing.CountY <= 1 ) {
return $"{title} — one column, {standing.Width:0} x {standing.Depth:0}";
}
return $"{title} — {standing.CountX} x {standing.CountY} · {MathF.Abs( standing.Spacing.x ):0} x {MathF.Abs( standing.Spacing.y ):0} bays";
}
protected override void OnClick( Vector2 point ) {
if ( gesture == PillarGesture.Span ) {
Bridge( point );
return;
}
Place( Seated( point ), Lone() );
}
void Bridge( Vector2 point ) {
if ( Spans.MergedClick( point ) is not { } span ) {
return;
}
Drew( span );
FinishDraft( span.Id );
Owner.Commit();
}
protected override void OnDrag( Vector2 from, Vector2 to ) {
Place( from, Bays( to - from ) );
}
void Place( Vector2 at, Grid bays ) {
Seeded();
var room = Owner.RoomOnLevel( at );
if ( room is null || Kind is null ) {
return;
}
var part = Standing( at, bays, room );
part.Id = Owner.Plan.AllocateId();
part.Name = $"Pillars{room.Pillars.Count + 1}";
room.Pillars.Add( part );
FinishDraft( part.Id );
Owner.Picked = new ArchSelection { Item = part, Room = room };
Drew( part );
Owner.Commit();
}
ArchPillarPart Standing( Vector2 from, Grid bays, ArchRoom room ) {
var part = draft.Section();
part.Type = Kind?.Name ?? draft.Type;
part.Origin = from;
part.BaseHeight = Owner.SeatAt( from, room, Beneath(), Upon() );
part.Placement = PillarPlacement.Grid;
part.CountX = bays.CountX;
part.CountY = bays.CountY;
part.Spacing = bays.Spacing;
return part;
}
float? Beneath() => Pointer.Found && Pointer.Face == ArchCursorFace.Ceiling ? Pointer.Height : null;
float? Upon() => Pointer.Found && Pointer.Face == ArchCursorFace.Floor ? Pointer.Height : null;
Grid Lone() => new( 1, 1, draft.Spacing );
readonly record struct Grid( int CountX, int CountY, Vector2 Spacing );
Grid Bays( Vector2 reach ) {
var limit = MathF.Max( 32f, maxSpacing > 1f ? maxSpacing : MathF.Max( draft.Spacing.x, draft.Spacing.y ) );
var alongX = Owner.Grid.DivideAtMost( Dragged( reach.x ), limit );
var alongY = Owner.Grid.DivideAtMost( Dragged( reach.y ), limit );
return new Grid(
alongX.Count + 1,
alongY.Count + 1,
new Vector2( MathF.Sign( reach.x ) * alongX.Step, MathF.Sign( reach.y ) * alongY.Step ) );
}
static float Dragged( float reach ) => MathF.Abs( reach ) < 1f ? 0f : MathF.Abs( reach );
protected override void BuildOptions( ToolSidebarWidget panel ) {
Seeded();
if ( Bridging && Spans.PickedName is { } picked ) {
panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {picked}. What is tuned here is also what the next bridge is drawn from." ) );
}
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Gesture", GestureName, GestureGlyph, PickGesture );
if ( Bridging ) {
Spans.MergedSteps( flow );
return;
}
flow.Step( "Type", Kind?.Title ?? "None", Kind?.Icon ?? "view_column", step => Browse( panel, step ) );
if ( gesture == PillarGesture.Grid ) {
flow.Step( "Bays", $"{Reach():0} max", "grid_on", BuildBays );
}
}
float Reach() => maxSpacing > 1f ? maxSpacing : MathF.Max( draft.Spacing.x, draft.Spacing.y );
string GestureName => Bridging ? "Span" : gesture == PillarGesture.Grid ? "Grid" : "Single";
string GestureGlyph => Bridging ? "bridge" : gesture == PillarGesture.Grid ? "grid_on" : "view_column";
void PickGesture( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "Single — click to stand one column", "pillar_single", "view_column",
!Bridging && gesture == PillarGesture.Single, () => Switch( PillarGesture.Single, step ) );
grid.Pick( "Grid — drag a colonnade, bays dividing the reach", "pillar_grid", "grid_on",
!Bridging && gesture == PillarGesture.Grid, () => Switch( PillarGesture.Grid, step ) );
grid.Pick( "Span — click two piers and it bridges them with a beam, an arch or a haunch", "pillar_span", "bridge",
Bridging, () => Switch( PillarGesture.Span, step ) );
}
void Switch( PillarGesture wanted, ArchWorkflowStep step ) {
gesture = wanted;
if ( wanted != PillarGesture.Span ) {
Spans.MergedCancel();
}
step.Chose();
}
void BuildBays( ArchWorkflowStep step ) {
step.Layout.Add( ArchPartUi.Number( "Max bay", maxSpacing, 240f, value => maxSpacing = value ) );
}
void Browse( ToolSidebarWidget panel, ArchWorkflowStep step ) {
var offered = Offered();
if ( offered.Count == 0 ) {
step.Layout.Add( ArchSidebarLayout.Advice( $"No pillar types are authored — add one to Assets/{ArchPillarCatalog.Directory}." ) );
return;
}
var shelf = new ArchPresetBrowser<ArchPillarType>( panel, Owner.Kit, Scope( "types" ), "Pillar types" ) {
Chosen = type => string.Equals( draft.Type, type.Name, StringComparison.OrdinalIgnoreCase ),
Choose = type => Adopt( type, step ),
Exports = "pillar",
Design = () => new ArchPillarDesigner( panel, Owner, draft, Kind, designed => ApplyDesigned( designed, step ) ).Show(),
Reload = () => {
Owner.ReloadPillarTypes();
Refresh();
}
};
shelf.DesignButton.Visible = true;
shelf.DesignButton.ToolTip = "Design a pillar type";
shelf.ReloadButton.Visible = true;
shelf.Set( offered );
step.Add( shelf );
}
List<ArchPresetItem<ArchPillarType>> Offered() {
var height = Owner.Kit.WallHeight;
return Owner.OfferedPillars()
.Select( type => new ArchPresetItem<ArchPillarType> {
Value = type,
Name = type.Title,
Detail = ArchPillarStage.Describe( type ),
Identity = ArchPillarStage.Identity( type.Standing( default, 0f, 0f ), height ),
Category = type.Column?.Span switch {
PillarSpan.Arch => "Arcade",
PillarSpan.Beam => "Beam",
_ => "Post"
},
Glyph = type.Icon,
Tags = $"{type.Name} {type.Description}",
View = ArchPresetPreview.Quarter,
Focus = ArchPreviewFocus.Whole,
Recipe = () => ArchPillarStage.Of( type.Standing( default, 0f, 0f ), height )
} )
.ToList();
}
void ApplyDesigned( ArchPillarPart designed, ArchWorkflowStep step ) {
draft = designed;
maxSpacing = MathF.Max( draft.Spacing.x, draft.Spacing.y );
step.Chose();
}
}