Editor code that builds a representation of a pipe bracket row for the level editor. It computes hanger stations, what pipe runs the bracket should hold based on spatial overlap and stacking order, and produces hanger geometry defaults when nothing is held.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A row of hangers standing in a dragged volume, sized by WHAT IS ACTUALLY IN IT. The volume says where the
// row runs and how often; every other number - how wide the cross-piece is, how far the drops reach, which
// lane each clamp closes on - is read off the runs crossing it, so adding a pipe to a bundle widens its
// hangers and moving the bundle moves them, with nothing to re-author either time.
//
// The same order rule the rest of the stack obeys decides what it holds: a bracket picks up the runs that
// were STANDING when it was made. To catch one added later, drag its row below that run in the stack - which
// is the whole of the non-destructive workflow, and needs no second control anywhere.
public sealed class ArchPipeBracketShape
{
public bool Stands { get; init; }
public ArchPipeFrame Frame { get; init; }
public List<float> Stations { get; init; } = new();
public List<ArchPipeShape> Held { get; init; } = new();
public ArchPipeHangerSpec Spec { get; init; } = new();
// What the row falls back to where it holds nothing yet: the box the author dragged, so an empty bracket
// volume shows the frame it will make rather than silently building nothing.
public float Depth { get; init; }
public int Count => Stations.Count;
public List<ArchPipeHold> Holding( float station )
{
var holds = ArchPipeHanger.Holding( Frame, station, Held );
if ( holds.Count > 0 )
{
return holds;
}
return new List<ArchPipeHold>
{
new()
{
Offset = 0f,
Lift = MathF.Max( 1f, Depth - Spec.Member ),
Radius = MathF.Max( 0f, Frame.Span * 0.5f - Spec.Overhang )
}
};
}
}
public static class ArchPipeBracket
{
public static ArchPipeBracketShape Resolve( ArchPlan plan, ArchPipeBracketPart part )
{
var frame = ArchPipeFrame.Of( part );
if ( !frame.Stands )
{
return new ArchPipeBracketShape();
}
return new ArchPipeBracketShape
{
Stands = true,
Frame = frame,
Stations = ArchDivide.AtMost( frame.Length, MathF.Max( 12f, part.Spacing ) ).Nodes.ToList(),
Held = Overlapping( plan, part ).Select( run => ArchPipeShape.Resolve( plan, run ) ).ToList(),
Spec = new ArchPipeHangerSpec
{
Kind = part.Kind,
Member = MathF.Max( 0.5f, part.MemberWidth ),
Clearance = MathF.Max( 0f, part.Clearance ),
Overhang = MathF.Max( 0f, part.Overhang ),
Detail = part.Detail
},
Depth = Depth( part )
};
}
// Standing in the box AND standing before it. Plan-wide, because a volume is world-space and a bracket
// under a party wall holds the runs of both units.
static IEnumerable<ArchPipePart> Overlapping( ArchPlan plan, ArchPipeBracketPart part )
{
if ( plan is null )
{
return Array.Empty<ArchPipePart>();
}
return plan.Buildings
.SelectMany( building => ArchLayerGate.Enabled( building.Pipes ) )
.Where( run => ArchLayerOrder.Applies( plan, part.Id, run.Id ) )
.Where( run => ArchPipeShape.Resolve( plan, run ).Path().Any( point =>
point.x >= part.Min.x && point.x <= part.Max.x
&& point.y >= part.Min.y && point.y <= part.Max.y
&& point.z >= part.BaseHeight && point.z <= part.TopHeight ) );
}
// How far the volume reaches along its own dodge axis - down off a ceiling, up off a floor, out of a wall.
static float Depth( ArchPipeBracketPart part )
{
if ( part.Mount != PipeMount.Wall )
{
return part.Rise;
}
var size = part.Size;
return MathF.Abs( part.AlongX ? size.y : size.x );
}
}