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.
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 );
}
}
}
}