Editor tool that draws a pipe route as a path and builds it from real fittings, straight lengths between circular elbows plus flanges, caps, valves and supports. Snaps new points to an angle step around the previous point, allows raising points, and can bolt a spool to free ends of nearby routes via PipeLinker.
namespace AdvancedMappingTools;
/// <summary>
/// Draws a pipe route and builds it from real fittings: straight lengths between circular
/// elbows, with flanges, caps, valves and supports. Unlike Pipe Run, the line is not swept
/// smooth - it is assembled, which is what pipework looks like. Points may be lifted with
/// Shift and dragged, so a route can leave the floor, run along a ceiling and come down again.
/// </summary>
[ToolCategory( "Industrial" )]
[Title( "Pipe Route" ), Icon( "plumbing" )]
public sealed class PipeRouteEditor : PathEditor
{
public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "CurveSegments", "Variation", "Smooth", nameof( AngleStep ) };
readonly PipeRouteGenerator _generator = new();
public PipeRouteEditor( PrimitiveTool tool ) : base( tool ) { }
public PipeRouteSettings Settings { get; } = new();
public override object ExtraSettings => Settings;
[Property, Group( "Routing" ), Title( "Angle Snap" ), Range( 0, 90, slider: false ), WideMode, Description( "Locks a new point to steps of this angle around the one before it. 45 gives the usual 45 and 90 degree runs, zero draws freely." )]
public float RouteAngle { get; set; } = 45.0f;
[Property, Group( "Routing" ), Title( "Connect Pipes" ), WideMode, Description( "After placing, look for free ends of other pipe routes near the two ends and bolt a spool between them." )]
public bool Connect { get; set; } = true;
[Property, ShowIf( nameof( Connect ), true ), Group( "Routing" ), Title( "Connect Radius" ), Range( 4, 512, slider: false ), WideMode, Description( "How far away a free end of another route may be to be joined." )]
public float ConnectRadius { get; set; } = 64.0f;
protected override int MinPoints => 2;
protected override bool PlacesObjects => true;
protected override bool AllowsHeight => true;
/// <summary>
/// Corners are the whole point of this tool, so no curve handles: every corner becomes
/// an elbow with the radius from the settings.
/// </summary>
protected override bool UsesBends => false;
protected override IEnumerable<string> Hints =>
[
"Click: place a point",
"Shift + drag a point: raise or lower it",
"Every corner becomes an elbow with the bend radius",
"Enter: create, Esc: stop"
];
protected override void BuildMesh( MeshKit kit ) { }
/// <summary>
/// Pull a new point onto a step of <see cref="RouteAngle"/> around the point before it,
/// so runs stay at 45 and 90 degrees instead of drifting a degree or two.
/// </summary>
protected override Vector3 Dock( Vector3 point )
{
var snapped = base.Dock( point );
if ( RouteAngle <= 0.0f || Points.Count == 0 ) return snapped;
var origin = Points[Points.Count - 1].Position;
var flat = ( snapped - origin ).WithZ( 0.0f );
if ( flat.Length < 1.0f ) return snapped;
var step = RouteAngle.DegreeToRadian();
var angle = MathF.Round( MathF.Atan2( flat.y, flat.x ) / step ) * step;
var locked = new Vector3( MathF.Cos( angle ), MathF.Sin( angle ), 0.0f ) * flat.Length;
return origin + locked + Vector3.Up * ( snapped.z - origin.z );
}
void Prepare()
{
Settings.Path = Curve();
Settings.PipeMaterial = PartTool.Pick( Settings.PipeMaterial, SurfaceMaterial );
Settings.FittingMaterial = PartTool.Pick( Settings.FittingMaterial, TrimMaterial );
Settings.SupportMaterial = PartTool.Pick( Settings.SupportMaterial, TrimMaterial );
Settings.InsulationMaterial = PartTool.Pick( Settings.InsulationMaterial, SurfaceMaterial );
}
protected override PolygonMesh BuildPreview()
{
Prepare();
return PartService.Plan( _generator, Settings, surface: SurfaceIntegration.Context( Settings, SceneEditorSession.Active?.Scene, Transform.Zero ) ).Flatten( new Transform( Vector3.Zero, Rotation.Identity ) );
}
public override bool ChangesOtherSettings( string property ) => property is nameof( PipeRouteSettings.FitSupportsToGround ) or nameof( PipeRouteSettings.ManualGroundLength ) or nameof( PipeRouteSettings.Supports ) or nameof( PipeRouteSettings.SupportMode ) or nameof( PipeRouteSettings.MountsEnabled ) or nameof( PipeRouteSettings.MountCollars ) or nameof( PipeRouteSettings.MountBolts ) or nameof( PipeRouteSettings.MountConnector );
public override void OnUpdate( SceneTrace trace )
{
base.OnUpdate( trace );
if ( Settings.SurfaceEnabled && Settings.ShowGroundDebug )
SurfaceDebug.Draw( PipeRouteSettings.GroundOwner );
if ( Settings.MountsEnabled && Settings.ShowMountDebug )
SurfaceDebug.Draw( Settings.MountOwner );
}
protected override void PlaceObjects()
{
Prepare();
PartTool.Place( _generator, Settings, new Transform( Vector3.Zero, Rotation.Identity ) );
if ( !Connect ) return;
var root = SceneEditorSession.Active.Selection.OfType<GameObject>().FirstOrDefault();
if ( root.IsValid() ) PipeLinker.Connect( root, ConnectRadius );
}
}