Editor-side type that builds a pipe shape for architecture pieces. It computes a centreline curve from authored nodes, steps and fillets profiles around blocks, computes supports/joints, and provides sampling helpers (At, Path, HangerFrame) used by editor tools to place geometry.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchPipeShape
{
public bool Stands { get; init; }
public ArchCurve Curve { get; init; }
public float Radius { get; init; }
public List<Vector3> Centreline { get; init; } = new();
public List<float> Joints { get; init; } = new();
public List<float> Supports { get; init; } = new();
public List<ArchPipeBlock> Blocks { get; init; } = new();
public List<float> Lanes { get; } = new() { 0f };
public int Count => Stands ? 1 : 0;
public float Width => Radius * 2f;
public float Rest => Radius;
public float Length => Curve?.Length ?? 0f;
public List<Vector3> Path( float offset = 0f )
{
if ( MathF.Abs( offset ) < 0.001f || Curve is null )
{
return new List<Vector3>( Centreline );
}
return Curve.Walk( 4f ).Select( frame => frame.Side( offset ) ).ToList();
}
public Vector3 At( float station, float offset = 0f )
{
if ( Curve?.Sample( station, out var frame ) == true )
{
return frame.Side( offset );
}
return Centreline.FirstOrDefault();
}
public ArchPipeFrame HangerFrame( ArchPipePart part, float station )
{
if ( Curve?.Sample( station, out var frame ) != true )
{
return default;
}
var outward = ArchPipe.SurfaceNormal( part, frame.Position );
var side = Side( part, frame, outward );
return new ArchPipeFrame
{
Stands = true,
Origin = frame.Position - outward * Radius,
Along = frame.Along,
Side = side,
Out = outward,
Length = Length,
Span = Radius * 2f
};
}
public static ArchPipeShape Resolve( ArchPlan plan, ArchPipePart part )
{
var authored = ArchCurve.Polyline( Canonical( plan, part ) );
if ( !authored.IsUsable )
{
return new ArchPipeShape();
}
var radius = part.Radius;
var blocks = ArchPipeCross.Blocking( plan, part, authored );
var joints = new List<float>();
var profile = Stepped( authored.Length, blocks, radius, joints );
var walked = authored.Walk( 4f, false, 1f, 3, profile.Select( point => point.x ) );
var points = walked.Select( frame => Routed( part, frame, LiftAt( profile, frame.Distance ), radius ) ).ToList();
var rounded = Rounded( points, Bend( part ), part.Detail.BendSegments );
var curve = ArchCurve.Polyline( rounded );
return new ArchPipeShape
{
Stands = curve.IsUsable,
Curve = curve,
Radius = radius,
Centreline = rounded,
Joints = joints,
Supports = part.Supports
? ArchDivide.AtMost( curve.Length, MathF.Max( 12f, part.SupportSpacing ) ).Nodes.ToList()
: new List<float>(),
Blocks = blocks
};
}
static IEnumerable<Vector3> Canonical( ArchPlan plan, ArchPipePart part )
{
foreach ( var asked in part.Nodes )
{
var shared = ArchPipe.All( plan )
.SelectMany( run => run.Nodes )
.FirstOrDefault( node => asked.Id != 0 && node.Id == asked.Id );
yield return shared?.Position ?? asked.Position;
}
}
static Vector3 Routed( ArchPipePart part, ArchFrame frame, float lift, float rest )
{
var outward = ArchPipe.SurfaceNormal( part, frame.Position );
var axis = part.Dodge switch
{
PipeDodge.Toward => -outward,
PipeDodge.Side => Side( part, frame, outward ),
_ => outward
};
return frame.Position + outward * rest + axis * (lift - rest);
}
static Vector3 Side( ArchPipePart part, ArchFrame frame, Vector3 outward )
{
if ( part.Mount == PipeMount.Wall )
{
return Vector3.Up;
}
var side = Vector3.Cross( frame.Along, outward ).Normal;
return side.IsNearZeroLength ? frame.Across : side;
}
static List<Vector2> Stepped( float length, List<ArchPipeBlock> blocks, float rest, List<float> joints )
{
var points = new List<Vector2> { new( 0f, rest ) };
foreach ( var block in blocks )
{
var ramp = MathF.Max( rest * 2f, block.Clear - rest );
points.Add( new Vector2( block.From - ramp, rest ) );
points.Add( new Vector2( block.From, block.Clear ) );
points.Add( new Vector2( block.To, block.Clear ) );
points.Add( new Vector2( block.To + ramp, rest ) );
}
points.Add( new Vector2( length, rest ) );
var walked = Clamped( points, length );
joints.AddRange( walked.Skip( 1 ).Take( Math.Max( 0, walked.Count - 2 ) ).Select( point => point.x ) );
return Filleted( walked, rest, 1 );
}
static float LiftAt( IReadOnlyList<Vector2> profile, float station )
{
for ( var index = 1; index < profile.Count; index++ )
{
var near = profile[index - 1];
var far = profile[index];
if ( station > far.x || far.x - near.x < 0.001f )
{
continue;
}
return MathX.Lerp( near.y, far.y, ((station - near.x) / (far.x - near.x)).Clamp( 0f, 1f ) );
}
return profile.Count == 0 ? 0f : profile[^1].y;
}
static List<Vector2> Clamped( List<Vector2> points, float length )
{
var kept = new List<Vector2>();
foreach ( var point in points )
{
var station = point.x.Clamp( 0f, length );
var lift = point.y;
while ( kept.Count > 0 && kept[^1].x >= station - 0.01f )
{
lift = MathF.Max( lift, kept[^1].y );
kept.RemoveAt( kept.Count - 1 );
}
kept.Add( new Vector2( station, lift ) );
}
return kept;
}
static float Bend( ArchPipePart part ) => part.Content switch
{
PipeContent.Cable => part.Diameter * 6f,
PipeContent.Tray or PipeContent.Duct => part.Diameter * 0.5f,
_ => part.Diameter * 1.5f
};
public static List<Vector2> Filleted( List<Vector2> points, float radius, int segments )
{
if ( segments < 1 || radius < 0.05f || points.Count < 3 )
{
return points;
}
var rounded = new List<Vector2> { points[0] };
for ( var index = 1; index < points.Count - 1; index++ )
{
var corner = points[index];
var back = points[index - 1] - corner;
var forward = points[index + 1] - corner;
if ( back.Length < 0.01f || forward.Length < 0.01f )
{
continue;
}
var into = back.Normal;
var away = forward.Normal;
var turn = MathF.Acos( Vector2.Dot( into, away ).Clamp( -1f, 1f ) );
if ( turn < 0.05f || turn > MathF.PI - 0.05f )
{
rounded.Add( corner );
continue;
}
var half = turn * 0.5f;
var tangent = MathF.Min( radius / MathF.Tan( half ), MathF.Min( back.Length, forward.Length ) * 0.5f );
var bisector = (into + away).Normal;
var centre = corner + bisector * (tangent / MathF.Cos( half ));
var start = corner + into * tangent - centre;
var finish = corner + away * tangent - centre;
var sweep = MathF.Atan2( start.x * finish.y - start.y * finish.x, Vector2.Dot( start, finish ) );
for ( var step = 0; step <= segments; step++ )
{
var angle = sweep * ((float)step / segments);
var cos = MathF.Cos( angle );
var sin = MathF.Sin( angle );
rounded.Add( centre + new Vector2( start.x * cos - start.y * sin, start.x * sin + start.y * cos ) );
}
}
rounded.Add( points[^1] );
return rounded;
}
static List<Vector3> Rounded( List<Vector3> points, float radius, int segments )
{
if ( segments < 1 || radius < 0.05f || points.Count < 3 )
{
return points;
}
var rounded = new List<Vector3> { points[0] };
for ( var index = 1; index < points.Count - 1; index++ )
{
var corner = points[index];
var back = points[index - 1] - corner;
var forward = points[index + 1] - corner;
if ( back.Length < 0.01f || forward.Length < 0.01f )
{
continue;
}
var into = back.Normal;
var away = forward.Normal;
var turn = MathF.Acos( Vector3.Dot( into, away ).Clamp( -1f, 1f ) );
if ( turn < 0.05f || turn > MathF.PI - 0.05f )
{
rounded.Add( corner );
continue;
}
var half = turn * 0.5f;
var tangent = MathF.Min( radius / MathF.Tan( half ), MathF.Min( back.Length, forward.Length ) * 0.5f );
var from = corner + into * tangent;
var to = corner + away * tangent;
var bisector = (into + away).Normal;
var centre = corner + bisector * (tangent / MathF.Cos( half ));
var start = from - centre;
var finish = to - centre;
var plane = Vector3.Cross( start, finish ).Normal;
var sweep = MathF.Atan2( Vector3.Cross( start, finish ).Length, Vector3.Dot( start, finish ) );
for ( var step = 0; step <= segments; step++ )
{
var angle = sweep * ((float)step / segments);
var cos = MathF.Cos( angle );
var sin = MathF.Sin( angle );
var turned = start * cos + Vector3.Cross( plane, start ) * sin + plane * Vector3.Dot( plane, start ) * (1f - cos);
rounded.Add( centre + turned );
}
}
rounded.Add( points[^1] );
return rounded;
}
}