A readonly struct that represents a pipe mounting frame in world space. It stores origin, axis directions (Along, Side, Out), size (Length, Span) and provides methods to convert station/offset/lift to world position, compute reach and station from a point, determine dodge stepping direction, and construct a frame from bracket geometry or parameters.
using System;
using Sandbox;
namespace Sunless.Architecture;
// WHERE A SERVICE VOLUME'S MOUNTING PLANE IS AND WHICH WAY IS OUT OF IT - the one answer, because a run,
// its hangers, the ghost and the report all measure off it and a second opinion is a bracket that reaches
// for a pipe standing somewhere else.
//
// The three mounts are one frame with the axes swapped, not three cases: a ceiling spreads its lanes across
// and dodges DOWN, a floor spreads across and dodges UP, a wall stacks its lanes UP and dodges out of the
// face. Everything downstream works in (station, offset, lift) and never asks which mount it is on.
public readonly struct ArchPipeFrame
{
public bool Stands { get; init; }
public Vector3 Origin { get; init; }
public Vector3 Along { get; init; }
public Vector3 Side { get; init; }
public Vector3 Out { get; init; }
public float Length { get; init; }
public float Span { get; init; }
public Vector3 At( float station, float offset, float lift )
{
return Origin + Along * station + Side * offset + Out * lift;
}
// How far a world point stands along the dodge axis, measured off the mounting plane. A box's own reach
// is the largest of its corners', which is what a run has to get past to clear it.
public float Reach( Vector3 point, Vector3 axis ) => Vector3.Dot( point - Origin, axis );
public float Station( Vector3 point ) => Vector3.Dot( point - Origin, Along );
// The direction a dodge steps in. Away is out of the surface, Toward dives back into it, Side goes round
// rather than over - a cable tray with a ceiling too close to hump under.
public Vector3 Stepping( PipeDodge dodge ) => dodge switch
{
PipeDodge.Toward => -Out,
PipeDodge.Side => Side,
_ => Out
};
public static ArchPipeFrame Of( ArchPipeBracketPart part )
{
return Of( part.Min, part.Max, part.BaseHeight, part.TopHeight, part.AlongX, part.Mount, part.Facing );
}
public static ArchPipeFrame Of( Vector2 min, Vector2 max, float bottom, float top, bool alongX, PipeMount mount, Vector2 facing )
{
var along = alongX ? new Vector2( 1f, 0f ) : new Vector2( 0f, 1f );
var across = new Vector2( -along.y, along.x );
var from = Vector2.Dot( min, along );
var to = Vector2.Dot( max, along );
var near = Vector2.Dot( min, across );
var far = Vector2.Dot( max, across );
var length = to - from;
if ( length < 1f || top - bottom < 1f )
{
return default;
}
if ( mount == PipeMount.Wall )
{
// The face is whichever side of the box the run is strapped to, and out of it is the way the volume
// was told to face - snapped onto the across axis, because a corridor drawn on the grid has no third
// direction to lean in.
var sign = Vector2.Dot( facing, across ) >= 0f ? 1f : -1f;
var seat = along * from + across * (sign > 0f ? near : far);
return new ArchPipeFrame
{
Stands = true,
Origin = new Vector3( seat.x, seat.y, (bottom + top) * 0.5f ),
Along = new Vector3( along.x, along.y, 0f ),
Side = Vector3.Up,
Out = new Vector3( across.x * sign, across.y * sign, 0f ),
Length = length,
Span = top - bottom
};
}
var hangs = mount == PipeMount.Ceiling;
var centre = along * from + across * ((near + far) * 0.5f);
return new ArchPipeFrame
{
Stands = true,
Origin = new Vector3( centre.x, centre.y, hangs ? top : bottom ),
Along = new Vector3( along.x, along.y, 0f ),
Side = new Vector3( across.x, across.y, 0f ),
Out = hangs ? Vector3.Down : Vector3.Up,
Length = length,
Span = MathF.Abs( far - near )
};
}
}