Editor-side code that computes and emits pipe hanger geometry for an architectural tool. It finds where pipe runs cross a station plane, creates hold records with offset/lift/radius, and emits mesh geometry for various hanger types (trapeze, clamp, strap, rod) into an ArchMesh.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One lane crossing one station plane, in the holder's own frame. A hanger is sized off these and never off
// the box that was dragged, so a bundle that humps over an obstruction gets a longer drop exactly where it
// humps rather than a row of identical rods hanging in the air beside it.
public readonly struct ArchPipeHold
{
public float Offset { get; init; }
public float Lift { get; init; }
public float Radius { get; init; }
}
// What a hanger is made of. Carried rather than read off a part, because a run's own inline supports and the
// Brackets tool's volume are the same fabrication and must not be able to disagree about it.
public sealed class ArchPipeHangerSpec
{
public BracketKind Kind { get; init; } = BracketKind.Trapeze;
public float Member { get; init; } = 2f;
public float Clearance { get; init; } = 0.5f;
public float Overhang { get; init; } = 3f;
public ArchPipeDetail Detail { get; init; } = new();
}
// THE ONE HANGER. Whatever holds a service run up - the run's own supports, or a Brackets volume dragged
// across three of them - resolves to the same holds and comes out of the same emitter, so adding a pipe to a
// bundle widens its hangers with no second edit and no second copy of the fabrication.
public static class ArchPipeHanger
{
// Where every lane of every run crosses one station plane of the holder's frame. Read off the LANE PATHS
// the generator actually lays, so a hanger under a crossing run meets the pipe rather than the corridor.
public static List<ArchPipeHold> Holding( ArchPipeFrame frame, float station, IEnumerable<ArchPipeShape> runs )
{
var holds = new List<ArchPipeHold>();
foreach ( var run in runs.Where( shape => shape.Stands ) )
{
foreach ( var offset in run.Lanes )
{
Crossings( frame, station, run.Path( offset ), run.Radius, holds );
}
}
return holds;
}
static void Crossings( ArchPipeFrame frame, float station, IReadOnlyList<Vector3> path, float radius, List<ArchPipeHold> holds )
{
for ( var index = 1; index < path.Count; index++ )
{
var near = frame.Station( path[index - 1] ) - station;
var far = frame.Station( path[index] ) - station;
if ( near * far > 0f && MathF.Abs( near ) > 0.01f && MathF.Abs( far ) > 0.01f )
{
continue;
}
var span = far - near;
var travel = MathF.Abs( span ) < 0.001f ? 0f : (-near / span).Clamp( 0f, 1f );
var at = Vector3.Lerp( path[index - 1], path[index], travel );
holds.Add( new ArchPipeHold
{
Offset = Vector3.Dot( at - frame.Origin, frame.Side ),
Lift = Vector3.Dot( at - frame.Origin, frame.Out ),
Radius = radius
} );
return;
}
}
public static void Emit( ArchMesh canvas, ArchPipeFrame frame, float station, IReadOnlyList<ArchPipeHold> holds, ArchPipeHangerSpec spec, ArchBrush brush )
{
if ( holds.Count == 0 )
{
return;
}
var member = MathF.Max( 0.5f, spec.Member );
var low = holds.Min( hold => hold.Offset - hold.Radius ) - spec.Overhang;
var high = holds.Max( hold => hold.Offset + hold.Radius ) + spec.Overhang;
var deep = holds.Max( hold => hold.Lift + hold.Radius ) + spec.Clearance;
switch ( spec.Kind )
{
case BracketKind.Clamp:
Clamps( canvas, frame, station, holds, spec, member, brush );
return;
case BracketKind.Strap:
Straps( canvas, frame, station, holds, member, brush );
return;
case BracketKind.Rod:
Drop( canvas, frame, station, (low + high) * 0.5f, deep + member, member, brush );
break;
default:
Drop( canvas, frame, station, low, deep + member, member, brush );
Drop( canvas, frame, station, high, deep + member, member, brush );
break;
}
Cross( canvas, frame, station, low, high, deep + member * 0.5f, member, brush );
}
// The threaded drop: from the surface it is fixed to, out to where the cross-piece hangs.
static void Drop( ArchMesh canvas, ArchPipeFrame frame, float station, float offset, float reach, float member, ArchBrush brush )
{
if ( reach < member )
{
return;
}
var path = new List<Vector3> { frame.At( station, offset, 0f ), frame.At( station, offset, reach ) };
canvas.Extrude( path, ArchPipeSection.Bar( member * 0.6f, member * 0.6f ), 1f, Rotation.Identity, brush );
}
static void Cross( ArchMesh canvas, ArchPipeFrame frame, float station, float low, float high, float lift, float member, ArchBrush brush )
{
if ( high - low < member )
{
return;
}
var path = new List<Vector3> { frame.At( station, low, lift ), frame.At( station, high, lift ) };
canvas.Extrude( path, ArchPipeSection.Bar( member, member * 0.5f ), 1f, Rotation.Identity, brush );
}
// A collar round the pipe and a short tab back to the surface - what a single run gets where there is no
// bundle worth a trapeze under it.
static void Clamps( ArchMesh canvas, ArchPipeFrame frame, float station, IReadOnlyList<ArchPipeHold> holds, ArchPipeHangerSpec spec, float member, ArchBrush brush )
{
var sides = Math.Max( ArchPipeSection.LeastSides, spec.Detail.Sides );
foreach ( var hold in holds )
{
var ring = new List<Vector3>
{
frame.At( station - member * 0.5f, hold.Offset, hold.Lift ),
frame.At( station + member * 0.5f, hold.Offset, hold.Lift )
};
canvas.Extrude( ring, ArchPipeSection.Round( hold.Radius + member * 0.35f, sides ), 1f, Rotation.Identity, brush );
var tab = new List<Vector3>
{
frame.At( station, hold.Offset, 0f ),
frame.At( station, hold.Offset, MathF.Max( 0.5f, hold.Lift - hold.Radius ) )
};
canvas.Extrude( tab, ArchPipeSection.Bar( member, member * 0.4f ), 1f, Rotation.Identity, brush );
}
}
// A flat bar from the face out to the pipe, which is how a run strapped to a wall is held.
static void Straps( ArchMesh canvas, ArchPipeFrame frame, float station, IReadOnlyList<ArchPipeHold> holds, float member, ArchBrush brush )
{
foreach ( var hold in holds )
{
var path = new List<Vector3>
{
frame.At( station, hold.Offset, 0f ),
frame.At( station, hold.Offset, hold.Lift + hold.Radius )
};
canvas.Extrude( path, ArchPipeSection.Bar( member, member * 0.4f ), 1f, Rotation.Identity, brush );
}
}
}