Editor/Tool/Subtools/ArchBracketSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// A row of hangers, dragged as a volume like everything else here. Nothing about it is typed twice: the box
// says where the row runs and how often, and every member is sized off the runs actually crossing it - so a
// pipe added to the bundle widens the cross-piece with no second edit, and moving the bundle moves the
// hangers with it.
[Title( "Brackets" ), Icon( "build" ), Group( "12" )]
public sealed class ArchBracketSubtool( ArchTool owner ) : ArchSubtool( owner ) {
public override ArchSurface[] Surfaces => new[] { ArchSurface.Bracket, ArchSurface.Pipework, ArchSurface.Cabling };
readonly ArchPipeBracketPart draft = new();
PipeMount? aim;
ArchCursorFace face = ArchCursorFace.Ceiling;
float depth = ArchPipe.DefaultDepth;
protected override bool Adjusts => true;
protected override ArchKind? DraftKind => ArchKind.Bracket;
protected override string Title() => "Brackets";
protected override string Advice() => "Drag a box across the runs to hold.";
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
PipeMount Mount() => ArchPipe.Aimed( aim, face );
void Aiming() {
if ( Owner.Cursor( out var cursor ) ) {
face = cursor.Face;
}
}
ArchPipeDrag Sketch( Vector2 from, Vector2 to ) {
var floor = Owner.LevelHeight;
var min = Min( from, to );
var max = Max( from, to );
return ArchPipe.Sketch( Owner.Grid, Owner.Plan, from, to, Mount(), floor, Soffit( min, max, floor ), depth );
}
float Soffit( Vector2 min, Vector2 max, float floor ) {
var centre = (min + max) * 0.5f;
if ( Owner.RoomOnLevel( centre ) is { } room && Owner.Plan.OwnerOf( room ) is { } building ) {
return ArchFloorGen.Overhead( Owner.Plan, building, room, Owner.Kit, centre );
}
return floor + Owner.Kit.WallHeight - ArchFloorGen.CeilingDepth( Owner.Kit );
}
protected override void DrawHover( Vector2 point ) {
Aiming();
base.DrawHover( point );
}
protected override void DrawPreview() {
Aiming();
var drag = Sketch( DragStart, DragCurrent );
var shape = ArchPipeBracket.Resolve( Owner.Plan, Previewed( drag ) );
ArchGhost.Volume( drag.Min, drag.Max, drag.Bottom, drag.Top );
foreach ( var station in shape.Stations ) {
var holds = shape.Holding( station );
var low = holds.Min( hold => hold.Offset - hold.Radius ) - shape.Spec.Overhang;
var high = holds.Max( hold => hold.Offset + hold.Radius ) + shape.Spec.Overhang;
var lift = holds.Max( hold => hold.Lift + hold.Radius ) + shape.Spec.Clearance;
ArchGhost.Path( new List<Vector3>
{
shape.Frame.At( station, low, 0f ),
shape.Frame.At( station, low, lift ),
shape.Frame.At( station, high, lift ),
shape.Frame.At( station, high, 0f )
}, false );
}
ArchGhost.Note( new Vector3( drag.Max.x, drag.Max.y, drag.Top ),
$"{shape.Count} hangers · holds {shape.Held.Count} runs" );
}
// Standing nowhere in the plan yet, so the overlap pass reads every corridor as already standing - which
// is what a bracket about to be placed at the top of the stack actually is.
ArchPipeBracketPart Previewed( ArchPipeDrag drag ) {
return new ArchPipeBracketPart {
Id = 0,
Kind = draft.Kind,
Mount = drag.Mount,
Min = drag.Min,
Max = drag.Max,
BaseHeight = drag.Bottom,
TopHeight = drag.Top,
AlongX = drag.AlongX,
Outward = drag.Outward,
Spacing = draft.Spacing,
MemberWidth = draft.MemberWidth,
Clearance = draft.Clearance,
Overhang = draft.Overhang,
Detail = draft.Detail
};
}
protected override void OnDrag( Vector2 from, Vector2 to ) {
var building = Owner.ActiveBuilding();
var drag = Sketch( from, to );
if ( ArchPipe.Hang( Owner.Plan, building, Owner.Level, drag, draft ) is not { } placed ) {
Log.Info( $"Architecture: that drag is shorter than {ArchPipe.MinSize} inches either way, so there is no row in it." );
return;
}
FinishDraft( placed.Id );
Owner.Picked = new ArchSelection { Item = placed, Building = building };
Drew( placed );
Owner.Commit( "Place Brackets" );
}
// One list: what the row hangs from and what kind of hanger it is - both picks, so the sequence runs itself
// down to the three typed rows that can never answer. The face is asked only while seeding, because a row
// already standing carries the face it was dragged against.
protected override void BuildOptions( ToolSidebarWidget panel ) {
var picked = Editing();
var part = picked?.Item as ArchPipeBracketPart ?? draft;
var changed = picked is null ? null : (Action)(() => Owner.Touch( $"Edit {picked.Describe()}" ));
if ( picked is not null ) {
panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {picked.Describe()}." ) );
}
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
if ( picked is null ) {
flow.Step( "Mounted on", ArchPipeUi.MountName( aim ), ArchPipeUi.MountGlyph( aim ), PickMount );
}
flow.Step( "Hangers", part.Kind.ToString(), ArchPipeUi.KindGlyph( part.Kind ), step => PickKind( part, step, changed ) );
flow.Step( "Row", $"{part.Spacing:0} apart", "space_bar", step => BuildRow( part, picked, step, changed ) );
flow.Step( "Members", $"{part.MemberWidth:0.##} in", "straighten", step => ArchPipeUi.Members( step.Layout, part, changed ) );
flow.Step( "Detail", $"{part.Detail.Sides} sides", "tune", step => ArchPipeUi.Detail( step.Layout, part.Detail, Refresh, changed ) );
}
void PickMount( ArchWorkflowStep step ) {
ArchPipeUi.Mount( step.Layout, aim, chosen => { aim = chosen; step.Chose(); } );
}
void PickKind( ArchPipeBracketPart part, ArchWorkflowStep step, Action changed ) {
ArchPipeUi.Kinds( step.Layout, part.Kind, kind => { part.Kind = kind; changed?.Invoke(); step.Chose(); } );
}
void BuildRow( ArchPipeBracketPart part, ArchSelection picked, ArchWorkflowStep step, Action changed ) {
if ( picked is null ) {
step.Layout.Add( ArchPartUi.Number( "Depth off the surface", depth, ArchPipe.DefaultDepth,
value => depth = MathF.Max( 2f, value ) ) );
}
ArchPipeUi.Spacing( step.Layout, part, changed );
}
ArchSelection Editing() {
return Owner.Picked?.Item is ArchPipeBracketPart ? Owner.Picked : null;
}
}