Editor subtool for placing bracket rows (hangars) in the architecture editor. It sketches a drag volume, previews hangers and counts, resolves a bracket part, and on commit places a bracket part into the plan and updates UI/options.
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. It fills with hangers at the spacing below, each one sized to whatever crosses it — a box holding nothing yet builds to its own extents.";
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" );
}
protected override void BuildOptions( ToolSidebarWidget panel )
{
var editing = Editing();
ArchPipeUi.Mount( panel, aim, chosen => { aim = chosen; Refresh(); } );
panel.AddGroup( "Row" ).Add( ArchPartUi.Number( "Depth off the surface", depth, ArchPipe.DefaultDepth,
value => depth = MathF.Max( 2f, value ) ) );
if ( editing is null )
{
ArchPipeUi.Brackets( panel, draft, Refresh, null );
}
BuildPicked( panel, editing );
}
void BuildPicked( ToolSidebarWidget panel, ArchSelection editing )
{
if ( editing is null )
{
return;
}
ArchSidebarSection.Show( panel, Scope( "selected" ), $"Selected — {editing.Describe()}", group =>
group.Add( new Button( "Deselect", "close" ) { Clicked = () => { Owner.Select( null ); Refresh(); } } ) );
ArchPartUi.Sheet( panel, Owner, editing, Refresh );
}
ArchSelection Editing()
{
return Owner.Picked?.Item is ArchPipeBracketPart ? Owner.Picked : null;
}
}