Editor-side settings and helpers for the Advanced Mapping Tools pipe route generator. Adds ground fitting options to PipeRouteSettings, probes the scene surface to place post bases and conform footplates, builds sloped plate prisms, and computes contact geometry against routed pipe meshes.
namespace AdvancedMappingTools;
public sealed partial class PipeRouteSettings : ISurfaceAwareSettings
{
public const string GroundOwner = "Pipe Route Ground";
[Property, Group( "Surface & Ground" ), WideMode, Description( "Measure each post base along its existing local vertical axis. No usable hit keeps the configured reference height. Footplates remain flat unless their separate conforming option is enabled." )]
public bool FitSupportsToGround { get; set; }
[Property, Group( "Surface & Ground" ), WideMode, Description( "Conform single-post footplates to a sampled planar surface (up to 60 degrees relative to local up). Keeps the XY footprint and vertical plate thickness; fits the lower post end to the plate. Not used for rack posts, riser clamps or manual length. Discontinuous or missing surfaces retain flat plates." )]
public bool ConformFootplatesToGround { get; set; }
[Property, Group( "Surface & Ground" ), Range( .01f, 100000, slider: false ), WideMode]
public float GroundRange { get; set; } = 2048;
[Property, Group( "Surface & Ground" ), Range( 0, 4096, slider: false ), WideMode]
public float GroundMinimum { get; set; } = .01f;
[Property, Group( "Surface & Ground" ), Range( -4096, 4096, slider: false ), WideMode]
public float GroundOffset { get; set; }
[Property, Group( "Surface & Ground" ), WideMode]
public SurfaceSelection GroundSelection { get; set; }
[Property, Group( "Surface & Ground" ), WideMode]
public SurfaceKind GroundKind { get; set; }
[Property, Group( "Surface & Ground" ), WideMode]
public string GroundIncludeTags { get; set; } = "";
[Property, Group( "Surface & Ground" ), WideMode]
public string GroundExcludeTags { get; set; } = "";
[Property, Group( "Surface & Ground" ), WideMode]
public bool ManualGroundLength { get; set; }
[Property, Group( "Surface & Ground" ), Range( 1, 100000, slider: false ), WideMode, Description( "World distance from the upper post connection to its base, including the existing footplate." )]
public float GroundLength { get; set; } = 128;
[Property, Group( "Surface & Ground" ), WideMode]
public bool ShowGroundDebug { get; set; }
[System.Text.Json.Serialization.JsonIgnore, Property, ReadOnly, Group( "Surface & Ground" ), WideMode]
public string GroundStatus => SurfaceEnabled ? SurfaceDebug.Summary( GroundOwner ) : "Automatic support fitting off.";
[System.Text.Json.Serialization.JsonIgnore]
public bool SurfaceEnabled => Supports && FitSupportsToGround;
public SurfaceProbeOptions ProbeOptions() => new()
{
Enabled = SurfaceEnabled,
MaximumDistance = ManualGroundLength ? 2048 : GroundRange,
MinimumDistance = ManualGroundLength ? .01f : GroundMinimum,
Offset = GroundOffset,
Selection = ManualGroundLength ? SurfaceSelection.Nearest : GroundSelection,
Kind = ManualGroundLength ? SurfaceKind.Any : GroundKind,
IncludeTags = ManualGroundLength ? "" : GroundIncludeTags,
ExcludeTags = ManualGroundLength ? "" : GroundExcludeTags,
Manual = ManualGroundLength,
ManualDistance = GroundLength
};
}
public static class PipeGroundSupport
{
public static void Hide( HashSet<string> hidden, PipeRouteSettings s )
{
if ( !s.SurfaceEnabled || s.ManualGroundLength || s.SupportMode == PipeSupportMode.Rack )
hidden.Add( nameof( s.ConformFootplatesToGround ) );
if ( !s.Supports )
hidden.Add( nameof( s.FitSupportsToGround ) );
if ( !s.SurfaceEnabled )
hidden.UnionWith( [nameof( s.GroundRange ), nameof( s.GroundMinimum ), nameof( s.GroundOffset ), nameof( s.GroundSelection ), nameof( s.GroundKind ), nameof( s.GroundIncludeTags ), nameof( s.GroundExcludeTags ), nameof( s.ManualGroundLength ), nameof( s.GroundLength ), nameof( s.ShowGroundDebug )] );
if ( !s.ManualGroundLength )
hidden.Add( nameof( s.GroundLength ) );
else
hidden.UnionWith( [nameof( s.GroundRange ), nameof( s.GroundMinimum ), nameof( s.GroundSelection ), nameof( s.GroundKind ), nameof( s.GroundIncludeTags ), nameof( s.GroundExcludeTags )] );
}
/// <summary>Only the post base changes. Cast along the existing post axis so a transformed root cannot move a leg sideways.</summary>
public static float Resolve( GenContext context, PipeRouteSettings s, Vector3 position, float top, float clearance, float fallback )
{
if ( !s.SurfaceEnabled || context.Surface == null )
return fallback;
var surface = context.Surface;
var origin = surface.World.PointToWorld( position.WithZ( top ) );
var direction = surface.World.PointToWorld( position.WithZ( top ) - Vector3.Up ) - origin;
var sample = surface.Read.Probe( origin, direction, s.ProbeOptions() );
if ( !sample.Usable )
return fallback;
var local = surface.World.PointToLocal( sample.Position ).z;
return surface.Read.AcceptBelow( sample, local, top - clearance ) ? local : fallback;
}
/// <summary>Sample the whole plate footprint before trusting a tangent plane. Normal transforms as a covector.</summary>
public static Vector3? FootSlope( GenContext context, PipeRouteSettings s, Vector3 at, float ground, float top, float size )
{
if ( !s.SurfaceEnabled || !s.ConformFootplatesToGround || s.ManualGroundLength || s.SupportMode == PipeSupportMode.Rack || context.Surface == null )
return null;
var surface = context.Surface;
var center = surface.Read.Samples.LastOrDefault();
if ( center == null || center.Status != SurfaceStatus.Hit )
return null;
Vector3? Reject( string reason )
{
surface.Read.Samples.Add( center with { Status = SurfaceStatus.Rejected, Message = "Footplate remains flat: " + reason } );
return null;
}
var origin = surface.World.PointToWorld( Vector3.Zero );
var normal = new Vector3( (surface.World.PointToWorld( Vector3.Forward ) - origin).Dot( center.Normal ), (surface.World.PointToWorld( Vector3.Left ) - origin).Dot( center.Normal ), (surface.World.PointToWorld( Vector3.Up ) - origin).Dot( center.Normal ) );
if ( !SurfaceSolver.Finite( normal ) || normal.Length < .0001f || normal.z / normal.Length < .5f )
return Reject( "surface is too steep or has an invalid normal." );
var slope = new Vector3( -normal.x / normal.z, -normal.y / normal.z, 0 );
var w = MathF.Max( size, .5f );
var half = w * 2;
if ( ground + w + half * (MathF.Abs( slope.x ) + MathF.Abs( slope.y )) >= top - .05f )
return Reject( "plate would reach the upper connection." );
var direction = surface.World.PointToWorld( Vector3.Down ) - origin;
foreach ( var x in new[]
{
-half,
0f,
half
}
)
foreach ( var y in new[]
{
-half,
0f,
half
}
)
{
if ( x == 0 && y == 0 )
continue;
var local = at + new Vector3( x, y, 0 );
var sample = surface.Read.Probe( surface.World.PointToWorld( local.WithZ( top ) ), direction, s.ProbeOptions() );
var expected = surface.World.PointToWorld( local.WithZ( ground + x * slope.x + y * slope.y ) );
if ( sample.Status != SurfaceStatus.Hit || sample.Object != center.Object || sample.Normal.Dot( center.Normal ) < .999f || sample.Position.Distance( expected ) > .1f )
return Reject( "footprint samples are missing or not on one plane." );
}
return slope.LengthSquared > .00000001f ? slope : null;
}
/// <summary>A vertical prism with independent planar lower and upper ends. Ring order is outward.</summary>
public static void SlopedPrism( MeshKit kit, Vector3 center, float half, float bottom, Vector3 bottomSlope, float top, Vector3 topSlope )
{
var offsets = new[]
{
new Vector3( -half, -half, 0 ),
new Vector3( half, -half, 0 ),
new Vector3( half, half, 0 ),
new Vector3( -half, half, 0 )
};
var low = offsets.Select( d => new Vector3( center.x + d.x, center.y + d.y, bottom + d.Dot( bottomSlope ) ) ).ToArray();
var high = offsets.Select( d => new Vector3( center.x + d.x, center.y + d.y, top + d.Dot( topSlope ) ) ).ToArray();
kit.BeginPart();
kit.Face( low.Reverse().ToArray() );
kit.Face( high );
for ( var i = 0; i < 4; i++ )
{
var j = (i + 1) % 4;
kit.Face( low[i], low[j], high[j], high[i] );
}
}
public static void Record( GenContext context, PipeRouteSettings s )
{
if ( !s.SurfaceEnabled )
return;
if ( context.Surface != null )
SurfaceDebug.Record( PipeRouteSettings.GroundOwner, context.Surface.Read );
else
SurfaceDebug.Note( PipeRouteSettings.GroundOwner, "No scene context; configured support reference retained." );
}
/// <summary>Match the exact body/insulation part for each routed piece, so a lower return leg cannot be mistaken for this pipe.</summary>
public static Dictionary<RoutePiece, RouteMounting.Obstacle[]> ContactGeometry( PartPlan root, PipeRouteSettings s, IReadOnlyList<RoutePiece> pieces )
{
var meshes = RouteMounting.Obstacles( root ).ToDictionary( m => m.Id );
var result = new Dictionary<RoutePiece, RouteMounting.Obstacle[]>();
var insulated = s.Insulation && s.InsulationThickness > .01f;
var run = 0;
var bend = 0;
foreach ( var piece in pieces )
{
var id = piece.IsBend ? $"{(insulated ? "insulation_bend" : "bend")}{bend++:00}" : $"{(insulated ? "insulation_run" : "run")}{run++:00}";
result[piece] = meshes.TryGetValue( id, out var mesh ) ? [mesh] : [];
}
return result;
}
static List<Vector3> Clip( List<Vector3> polygon, Vector3 center, Vector3 axis, float limit )
{
var output = new List<Vector3>();
if ( polygon.Count == 0 )
return output;
var previous = polygon[^1];
var before = (previous - center).Dot( axis ) - limit;
foreach ( var current in polygon )
{
var after = (current - center).Dot( axis ) - limit;
var inBefore = before <= .00001f;
var inAfter = after <= .00001f;
if ( inBefore != inAfter )
{
var denominator = before - after;
if ( MathF.Abs( denominator ) > .000001f )
output.Add( Vector3.Lerp( previous, current, before / denominator ) );
}
if ( inAfter )
output.Add( current );
previous = current;
before = after;
}
return output;
}
/// <summary>Lowest real pipe surface inside the crossbeam's footprint. This gives physical contact without choosing an arbitrary polygon fan.</summary>
public static float ContactDrop( IReadOnlyList<RouteMounting.Obstacle> meshes, Vector3 point, Vector3 axis, float radius, float size, float rackWidth, out Vector3 contact )
{
contact = point - Vector3.Up * radius;
if ( meshes == null )
return radius;
var along = axis.WithZ( 0 ).Normal;
var across = Vector3.Cross( Vector3.Up, axis ).Normal;
if ( along.Length < .5f || across.Length < .5f )
return radius;
var half = MathF.Max( rackWidth, radius * 3 ) * .5f;
var width = MathF.Max( size, .5f ) * .4f;
var lowest = float.PositiveInfinity;
foreach ( var mesh in meshes )
{
if ( !mesh.Complete )
continue;
foreach ( var triangle in mesh.Triangles )
{
var polygon = new List<Vector3>
{
triangle.A,
triangle.B,
triangle.C
};
polygon = Clip( polygon, point, along, width );
polygon = Clip( polygon, point, -along, width );
polygon = Clip( polygon, point, across, half );
polygon = Clip( polygon, point, -across, half );
foreach ( var vertex in polygon )
if ( vertex.z < lowest )
{
lowest = vertex.z;
contact = vertex;
}
}
}
var drop = point.z - lowest;
if ( !float.IsFinite( drop ) || drop <= .0001f )
{
contact = point - Vector3.Up * radius;
return radius;
}
return drop;
}
}