Editor utility for pipe routing collision in architectural plans. It finds obstacles (other pipes, beams, pillars, platforms) intersecting a proposed pipe route, computes required clearance ranges along the route, and merges adjacent blocking segments into ArchPipeBlock entries.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchPipeBlock
{
public int Id { get; init; }
public string Name { get; init; } = "";
public float From { get; init; }
public float To { get; init; }
public float Clear { get; init; }
}
public static class ArchPipeCross
{
readonly record struct Obstacle( int Id, string Name, BBox Bounds );
public static List<ArchPipeBlock> Blocking( ArchPlan plan, ArchPipePart mine, ArchCurve route )
{
if ( plan is null || route?.IsUsable != true || mine.Dodge == PipeDodge.None )
{
return new List<ArchPipeBlock>();
}
var blocks = Solids( plan, mine )
.Select( obstacle => Crossing( mine, route, obstacle ) )
.Where( block => block is not null )
.ToList();
return Merged( blocks, mine.Radius );
}
static ArchPipeBlock Crossing( ArchPipePart mine, ArchCurve route, Obstacle obstacle )
{
var reach = mine.Radius + mine.Clearance;
var bounds = new BBox( obstacle.Bounds.Mins - reach, obstacle.Bounds.Maxs + reach );
var frames = route.Walk( MathF.Max( 1f, mine.Radius ) )
.Where( frame => Contains( bounds, frame.Position ) )
.ToList();
if ( frames.Count == 0 )
{
return null;
}
var clear = mine.Radius;
foreach ( var frame in frames )
{
var outward = ArchPipe.SurfaceNormal( mine, frame.Position );
var axis = mine.Dodge switch
{
PipeDodge.Toward => -outward,
PipeDodge.Side => Vector3.Cross( frame.Along, outward ).Normal,
_ => outward
};
foreach ( var corner in Corners( obstacle.Bounds ) )
{
clear = MathF.Max( clear, Vector3.Dot( corner - frame.Position, axis ) + mine.Clearance + mine.Radius );
}
}
if ( clear <= mine.Radius + 0.25f )
{
return null;
}
return new ArchPipeBlock
{
Id = obstacle.Id,
Name = obstacle.Name,
From = frames.Min( frame => frame.Distance ),
To = frames.Max( frame => frame.Distance ),
Clear = clear
};
}
static IEnumerable<Obstacle> Solids( ArchPlan plan, ArchPipePart mine )
{
var shared = mine.Nodes.Where( node => node.Id != 0 ).Select( node => node.Id ).ToHashSet();
foreach ( var other in ArchPipe.All( plan ).Where( other => other.Id != mine.Id && ArchLayerOrder.Applies( plan, mine.Id, other.Id ) ) )
{
if ( other.Nodes.Any( node => shared.Contains( node.Id ) ) )
{
continue;
}
var points = other.Nodes.Select( node => node.Position + ArchPipe.SurfaceNormal( other, node.Position ) * other.Radius ).ToList();
for ( var index = 1; index < points.Count; index++ )
{
yield return new Obstacle( other.Id, other.Name, Around( points[index - 1], points[index], other.Radius ) );
}
}
foreach ( var room in plan.AllRooms() )
{
foreach ( var beam in room.Beams.Where( beam => ArchLayerOrder.Applies( plan, mine.Id, beam.Id ) ) )
{
yield return new Obstacle( beam.Id, beam.Name, Bounds( beam.Outline(), beam.Soffit, beam.TopHeight ) );
}
foreach ( var pillar in room.Pillars.Where( pillar => ArchLayerOrder.Applies( plan, mine.Id, pillar.Id ) ) )
{
foreach ( var position in ArchAsks.PillarPositions( pillar ) )
{
var half = pillar.Half;
var mins = new Vector3( position.x - half.x, position.y - half.y, pillar.BaseHeight );
var maxs = new Vector3( position.x + half.x, position.y + half.y, pillar.BaseHeight + MathF.Max( 1f, pillar.Height ) );
yield return new Obstacle( pillar.Id, pillar.Name, new BBox( mins, maxs ) );
}
}
}
foreach ( var building in plan.Buildings )
{
foreach ( var platform in building.Platforms.Where( platform => ArchLayerOrder.Applies( plan, mine.Id, platform.Id ) ) )
{
yield return new Obstacle( platform.Id, platform.Name, Bounds( platform.Outline(), platform.GradeHeight, platform.TopHeight ) );
}
}
}
static BBox Around( Vector3 from, Vector3 to, float radius )
{
var reach = new Vector3( radius, radius, radius );
return new BBox( Vector3.Min( from, to ) - reach, Vector3.Max( from, to ) + reach );
}
static BBox Bounds( IReadOnlyList<Vector2> outline, float bottom, float top )
{
ArchFootprint.Bounds( outline, out var min, out var max );
return new BBox( new Vector3( min.x, min.y, bottom ), new Vector3( max.x, max.y, top ) );
}
static bool Contains( BBox bounds, Vector3 point )
{
return point.x >= bounds.Mins.x && point.x <= bounds.Maxs.x
&& point.y >= bounds.Mins.y && point.y <= bounds.Maxs.y
&& point.z >= bounds.Mins.z && point.z <= bounds.Maxs.z;
}
static IEnumerable<Vector3> Corners( BBox bounds )
{
foreach ( var x in new[] { bounds.Mins.x, bounds.Maxs.x } )
{
foreach ( var y in new[] { bounds.Mins.y, bounds.Maxs.y } )
{
yield return new Vector3( x, y, bounds.Mins.z );
yield return new Vector3( x, y, bounds.Maxs.z );
}
}
}
static List<ArchPipeBlock> Merged( List<ArchPipeBlock> blocks, float rest )
{
var ordered = blocks.OrderBy( block => block.From ).ToList();
var merged = new List<ArchPipeBlock>();
foreach ( var block in ordered )
{
if ( merged.Count > 0 && block.From <= merged[^1].To + rest * 2f )
{
var held = merged[^1];
merged[^1] = new ArchPipeBlock
{
Id = held.Id,
Name = held.Name,
From = held.From,
To = MathF.Max( held.To, block.To ),
Clear = MathF.Max( held.Clear, block.Clear )
};
continue;
}
merged.Add( block );
}
return merged;
}
}