Editor/Pipe/ArchPipeGen.cs

Editor generator for pipe-like architectural parts. It chooses materials, builds the run mesh, fittings (collars) at couplings and bends, and support hangers into an ArchMesh using shape and part data.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchPipeGen
{
	public static void Build( ArchMesh canvas, ArchPlan plan, ArchPipePart part, ArchStyle style, ArchPalette host )
	{
		var shape = ArchPipeShape.Resolve( plan, part );

		if ( !shape.Stands )
		{
			return;
		}

		var run = Indexed( part, part.RunMaterialIndex, style.Brush( Surface( part ), part.Palette, host ) );
		var fittings = Indexed( part, part.FittingMaterialIndex, style.Brush( Surface( part ), part.Palette, host ) );
		var supports = Indexed( part, part.SupportMaterialIndex, style.Brush( ArchSurface.Bracket, part.Palette, host ) );

		Run( canvas, shape, part, run );
		Fittings( canvas, plan, shape, part, fittings );
		Supports( canvas, shape, part, supports );
	}

	// A pipe and a duct are plumbing; a conduit, a cable and the tray it lies in are electrical. Two roles,
	// so a map can paint a red rainwater run and a grey services run apart without touching either part.
	public static ArchSurface Surface( ArchPipePart part ) => part.Content switch
	{
		PipeContent.Conduit or PipeContent.Cable or PipeContent.Tray => ArchSurface.Cabling,
		_ => ArchSurface.Pipework
	};

	static ArchBrush Indexed( ArchPipePart part, int index, ArchBrush fallback )
	{
		var path = part.MaterialAt( index );

		if ( string.IsNullOrWhiteSpace( path ) )
		{
			return fallback;
		}

		return new ArchBrush
		{
			Material = ArchStyle.Load( path ),
			TexelScale = fallback.TexelScale,
			Shift = fallback.Shift
		};
	}

	// The run's own body stays on the HOST canvas - a generator that put everything into named pieces leaves
	// an empty host, which the cache reads as a part that has vanished and rebuilds the whole scene cold.
	static void Run( ArchMesh canvas, ArchPipeShape shape, ArchPipePart part, ArchBrush brush )
	{
		canvas.Extrude( shape.Path(), ArchPipeSection.Of( part ), 1f, Rotation.Identity, brush );
	}

	// Collars down the run and a joint at every turn. Round stock only: a tray and a duct are fabricated in
	// lengths and flanged, not socketed, so a ring round one reads as a mistake.
	static void Fittings( ArchMesh canvas, ArchPlan plan, ArchPipeShape shape, ArchPipePart part, ArchBrush brush )
	{
		var stations = Fixings( plan, shape, part );

		if ( stations.Count == 0 )
		{
			return;
		}

		var section = ArchPipeSection.Of( part );
		var width = MathF.Max( 1f, part.Radius * 0.8f );

		using ( canvas.Part( ArchPieces.Fittings ) )
		{
			foreach ( var station in stations )
			{
				var ring = new List<Vector3>
				{
					shape.At( station - width * 0.5f ),
					shape.At( station + width * 0.5f )
				};

				canvas.Extrude( ring, section, 1.3f, Rotation.Identity, brush );
			}
		}
	}

	// Where every coupling down a run stands, so the generator and the report count the same fittings.
	public static List<float> Fixings( ArchPlan plan, ArchPipeShape shape, ArchPipePart part )
	{
		var stations = new List<float>();
		var length = shape.Length;

		if ( part.Detail.Couplings && part.Detail.CouplingSpacing >= 8f && length >= 16f )
		{
			stations.AddRange( ArchDivide.AtMost( length, part.Detail.CouplingSpacing ).Inner );
		}

		if ( part.Detail.BendCollars )
		{
			stations.AddRange( shape.Joints );
		}

		foreach ( var fitting in ArchPipeFittings.Resolve( plan, part ) )
		{
			var nearest = shape.Curve.Dense.OrderBy( frame => (frame.Position - fitting.Position).Length ).FirstOrDefault();

			stations.Add( nearest.Distance );
		}

		return stations
			.Where( station => station >= 0f && station <= length )
			.OrderBy( station => station )
			.Aggregate( new List<float>(), ( kept, station ) =>
			{
				if ( kept.Count == 0 || MathF.Abs( kept[^1] - station ) > 0.5f )
				{
					kept.Add( station );
				}

				return kept;
			} );
	}

	// The run's own hangers go through the one hanger emitter the Brackets tool uses, so a support under a
	// bundle and a bracket dragged across it are the same fabrication measured the same way.
	static void Supports( ArchMesh canvas, ArchPipeShape shape, ArchPipePart part, ArchBrush brush )
	{
		if ( shape.Supports.Count == 0 )
		{
			return;
		}

		var spec = new ArchPipeHangerSpec
		{
			Kind = part.SupportKind,
			Member = MathF.Max( 1f, part.Radius * 0.8f ),
			Clearance = 0.5f,
			Overhang = MathF.Max( 1f, part.Radius ),
			Detail = part.Detail
		};

		using ( canvas.Part( ArchPieces.Brackets ) )
		{
			foreach ( var station in shape.Supports )
			{
				var frame = shape.HangerFrame( part, station );
				var holds = new List<ArchPipeHold>
				{
					new()
					{
						Offset = 0f,
						Lift = part.Radius,
						Radius = part.Radius
					}
				};

				ArchPipeHanger.Emit( canvas, frame, 0f, holds, spec, brush );
			}
		}
	}
}