Editor/Pipe/ArchPipeCross.cs

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.

File AccessNetworking
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;
	}
}