Editor/Pipe/ArchPipeBracket.cs
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 );
}
}