editor/AdvancedMappingTools/Framework/RouteMounting.cs

Editor-side helper for the advanced mapping tools. Defines the mount direction enum, the IRouteAnchorSettings interface and an AnchorCollar record, then computes mount/collar sites along cable and pipe routes, tests connector rays against a planned assembly's triangles and bounding boxes, and runs a separating-axis test for overlapping mounting plates.

namespace AdvancedMappingTools;
public enum RouteMountDirection
{
	Back,
	Front,
	Left,
	Right,
	Floor,
	Ceiling
}

public interface IRouteAnchorSettings : IAnchorAwareSettings
{
	RouteMountDirection MountDirection { get; }

	float MountSpacing { get; }

	float MountPhase { get; }

	bool MountAvoidAssembly { get; }

	bool MountCollars { get; }

	float MountCollarWidth { get; }

	float MountCollarThickness { get; }
}

/// <summary>Local-space collar follows the actual polygonal section, including interpolated cable frames.</summary>
public sealed record AnchorCollar( Vector3 Center, Vector3 Left, Vector3 Up, Vector3 Axis, float Radius, float Width, float Thickness, int Sides )
{
	public Vector3 Point( int index, bool outer, float along )
	{
		var angle = index * MathF.PI * 2 / Sides;
		return Center + (Left * MathF.Cos( angle ) + Up * MathF.Sin( angle )) * (Radius + (outer ? Thickness : 0)) + Axis * along;
	}
}

public static class RouteMounting
{
	public static Vector3 Direction( RouteMountDirection direction ) => direction switch
	{
		RouteMountDirection.Front => new( 0, 1, 0 ),
		RouteMountDirection.Left => new( -1, 0, 0 ),
		RouteMountDirection.Right => new( 1, 0, 0 ),
		RouteMountDirection.Floor => Vector3.Down,
		RouteMountDirection.Ceiling => Vector3.Up,
		_ => new( 0, -1, 0 )};
	public static HashSet<string> Hidden( IRouteAnchorSettings s )
	{
		var hidden = AnchorIntegration.Hidden( s );
		if ( s is CableRouteSettings cable && (!cable.MountsEnabled || cable.Cables < 2) )
			hidden.Add( nameof( CableRouteSettings.MountStagger ) );
		if ( !s.MountsEnabled )
			hidden.UnionWith( ["MountAvoidAssembly", "MountDirection", "MountSpacing", "MountPhase", "MountCollars", "MountCollarWidth", "MountCollarThickness"] );
		if ( !s.MountCollars )
			hidden.UnionWith( ["MountCollarWidth", "MountCollarThickness"] );
		return hidden;
	}

	static bool Valid( IRouteAnchorSettings s ) => Enum.IsDefined( s.MountDirection ) && float.IsFinite( s.MountSpacing ) && s.MountSpacing >= 8 && s.MountSpacing <= 2048 && float.IsFinite( s.MountPhase ) && s.MountPhase >= 0 && s.MountPhase <= 1 && (!s.MountCollars || float.IsFinite( s.MountCollarWidth ) && s.MountCollarWidth >= .25f && s.MountCollarWidth <= 32 && float.IsFinite( s.MountCollarThickness ) && s.MountCollarThickness >= .1f && s.MountCollarThickness <= 16);
	static float CollarWidth( IRouteAnchorSettings s )
	{
		if ( s.MountCollars )
			return s.MountCollarWidth;
		var options = s.MountOptions();
		return options.Connector && float.IsFinite( options.ConnectorWidth ) ? MathF.Max( options.ConnectorWidth, .25f ) : .25f;
	}

	public static IEnumerable<AnchorSite> CableSites( CableRouteSettings s )
	{
		if ( !Valid( s ) )
			yield break;
		foreach ( var route in CableRouteGeometry.Tracks( s ) )
		{
			var track = route.Track;
			var count = track.Closed ? track.Count - 1 : track.Count;
			var points = track.Frames.Take( count ).Select( f => f.Position ).ToArray();
			var left = track.Frames.Take( count ).Select( f => f.Left * f.Stretch * f.Width ).ToArray();
			var up = track.Frames.Take( count ).Select( f => f.Up * f.Height ).ToArray();
			var corners = route.Corners.Where( i => i < track.Count ).Select( i => track.Frames[i].Distance ).ToArray();
			foreach ( var item in Samples( s, $"cable{route.Index}", points, left, up, track.Closed, Math.Clamp( s.Radius, .25f, 32 ), Math.Clamp( s.Sides, 4, 32 ), corners, MathF.Max( CollarWidth( s ) * .5f + 2, 4 ), [$"cable{route.Index}"], s.MountStagger && route.Index > 0 ? (s.MountPhase + route.Index / (float)Math.Clamp( s.Cables, 1, 12 )) % 1 : s.MountPhase ) )
				yield return item.Site;
		}
	}

	public static IEnumerable<AnchorSite> PipeSites( PipeRouteSettings s )
	{
		if ( !Valid( s ) || s.Path == null || s.Path.Nodes.Count < 2 )
			yield break;
		var pieces = PipeRouteGenerator.MountPieces( s );
		var total = PipeRoute.Total( pieces );
		var walked = 0f;
		var pieceIndex = 0;
		var runIndex = 0;
		var largest = MathF.Max( s.Size * .5f, .5f );
		if ( s.Sections != null )
			foreach ( var section in s.Sections )
				if ( section != null && float.IsFinite( section.Size ) )
					largest = MathF.Max( largest, section.Size * .5f );
		foreach ( var piece in pieces )
		{
			var offset = walked;
			walked += piece.Length;
			var id = pieceIndex++;
			if ( piece.IsBend )
				continue;
			var runId = runIndex++;
			if ( piece.Points.Count < 2 )
				continue;
			PipeRouteGenerator.Frames( piece.Points, piece.Closed, out var tangent, out var left );
			var up = tangent.Select( ( t, i ) => Vector3.Cross( t, left[i] ).Normal ).ToArray();
			var radius = piece.OverrideRadius > .01f ? piece.OverrideRadius : MathF.Max( s.Size * .5f, .5f );
			if ( s.Insulation && s.InsulationThickness > .01f )
				radius += s.InsulationThickness;
			var corners = new List<float>
			{
				0
			};
			for ( var i = 1; i < piece.Points.Count; i++ )
				corners.Add( corners[^1] + piece.Points[i - 1].Distance( piece.Points[i] ) );
			var guard = MathF.Max( CollarWidth( s ) * .5f + 2, MathF.Max( s.CapLength, largest * .75f + 2 ) );
			foreach ( var item in Samples( s, $"pipe{id}", piece.Points.ToArray(), left, up, piece.Closed, radius, Math.Clamp( s.Sides, 4, 48 ), corners.ToArray(), guard, [$"run{runId:00}", $"insulation_run{runId:00}"] ) )
			{
				var along = offset + item.Along;
				var keep = true;
				for ( var valve = 1; valve <= Math.Clamp( s.Valves, 0, 8 ); valve++ )
					if ( MathF.Abs( along - total * valve / (Math.Clamp( s.Valves, 0, 8 ) + 1f) ) < largest * 4 + CollarWidth( s ) )
						keep = false;
				if ( s.Branches != null )
					foreach ( var branch in s.Branches )
						if ( branch != null && MathF.Abs( along - total * Math.Clamp( branch.At, .02f, .98f ) ) < largest * 3 + CollarWidth( s ) )
							keep = false;
				if ( s.FlangeSpacing > 0 && float.IsFinite( s.FlangeSpacing ) )
				{
					var station = item.Along / s.FlangeSpacing;
					if ( MathF.Abs( station - MathF.Round( station ) ) * s.FlangeSpacing < largest + CollarWidth( s ) )
						keep = false;
				}

				if ( keep )
					yield return item.Site;
			}
		}
	}

	static IEnumerable<(AnchorSite Site, float Along)> Samples( IRouteAnchorSettings settings, string id, Vector3[] points, Vector3[] left, Vector3[] up, bool closed, float radius, int sides, float[] corners, float guard, string[] attachmentParts, float? phase = null )
	{
		if ( points.Length < 2 || points.Any( p => !SurfaceSolver.Finite( p ) ) || !float.IsFinite( radius ) || radius <= 0 )
			yield break;
		var segments = closed ? points.Length : points.Length - 1;
		var distance = new float[segments + 1];
		for ( var i = 0; i < segments; i++ )
			distance[i + 1] = distance[i] + points[i].Distance( points[(i + 1) % points.Length] );
		var total = distance[^1];
		if ( !float.IsFinite( total ) || total < .01f || total > 1000000 )
			yield break;
		var direction = Direction( settings.MountDirection );
		var at = MathF.Min( settings.MountSpacing, total ) * (phase ?? settings.MountPhase);
		var span = 0;
		var step = settings.MountSpacing;
		for ( var number = 0; at < total && number < 8192; number++, at += step )
		{
			if ( !closed && (at < guard || total - at < guard) )
				continue;
			if ( corners.Any( c => MathF.Abs( c - at ) < guard ) )
				continue;
			while ( span + 1 < segments && distance[span + 1] < at )
				span++;
			var length = distance[span + 1] - distance[span];
			if ( length < .01f )
				continue;
			var t = (at - distance[span]) / length;
			var next = (span + 1) % points.Length;
			var center = Vector3.Lerp( points[span], points[next], t );
			var x = Vector3.Lerp( left[span], left[next], t );
			var y = Vector3.Lerp( up[span], up[next], t );
			var axis = Vector3.Cross( x, y );
			if ( axis.Length < .1f )
				continue;
			axis = axis.Normal;
			var radial = direction - axis * direction.Dot( axis );
			if ( radial.Length < .05f )
				continue;
			radial = radial.Normal;
			var collar = new AnchorCollar( center, x, y, axis, radius, CollarWidth( settings ), settings.MountCollars ? settings.MountCollarThickness : 0, sides );
			var hit = float.PositiveInfinity;
			for ( var i = 0; i < sides; i++ )
			{
				var a = collar.Point( i, settings.MountCollars, 0 ) - center;
				var b = collar.Point( (i + 1) % sides, settings.MountCollars, 0 ) - center;
				var edge = b - a;
				var denominator = Vector3.Cross( radial, edge ).Dot( axis );
				if ( MathF.Abs( denominator ) < .00001f )
					continue;
				var ray = Vector3.Cross( a, edge ).Dot( axis ) / denominator;
				var u = Vector3.Cross( a, radial ).Dot( axis ) / denominator;
				if ( ray > 0 && u >= -.0001f && u <= 1.0001f )
					hit = MathF.Min( hit, ray );
			}

			if ( !float.IsFinite( hit ) )
				continue;
			yield return (new AnchorSite( $"{id}_at{number:0000}", center + radial * hit, direction ) { Collar = settings.MountCollars ? collar : null, AttachmentParts = attachmentParts }, at);
		}
	}

	public sealed record Triangle( Vector3 A, Vector3 B, Vector3 C );
	public sealed record Obstacle( string Id, BBox Bounds, Triangle[] Triangles, bool Complete = true );
	public static List<Obstacle> Obstacles( PartPlan root )
	{
		var list = new List<Obstacle>();
		void Visit( PartPlan part, Transform parent, bool first )
		{
			var transform = first ? Transform.Zero : parent.ToWorld( part.Local );
			if ( part.Mesh != null && part.Category != "mount" )
			{
				var faces = part.Mesh.FaceHandles.ToArray();
				if ( faces.Length > 0 && part.Mesh.CreateFace( faces[0], transform, Color.White ) == null )
					part.Mesh.Rebuild();
				var triangles = new List<Triangle>();
				var complete = true;
				foreach ( var face in faces )
				{
					var vertices = part.Mesh.CreateFace( face, transform, Color.White );
					if ( vertices == null )
					{
						complete = false;
						continue;
					}

					for ( var i = 0; i + 2 < vertices.Length; i += 3 )
						triangles.Add( new( vertices[i].Position, vertices[i + 1].Position, vertices[i + 2].Position ) );
				}

				list.Add( new( part.Id, part.Mesh.CalculateBounds( transform ), triangles.ToArray(), complete ) );
			}

			foreach ( var child in part.Children )
				Visit( child, transform, false );
		}

		Visit( root, Transform.Zero, true );
		return list;
	}

	static bool BoxHit( Vector3 a, Vector3 b, BBox box )
	{
		var low = 0f;
		var high = 1f;
		var delta = b - a;
		foreach ( var pair in new[]
		{
			(a.x, delta.x, box.Mins.x, box.Maxs.x),
			(a.y, delta.y, box.Mins.y, box.Maxs.y),
			(a.z, delta.z, box.Mins.z, box.Maxs.z)
		}

 )
		{
			if ( MathF.Abs( pair.Item2 ) < .00001f )
			{
				if ( pair.Item1 < pair.Item3 || pair.Item1 > pair.Item4 )
					return false;
				continue;
			}

			var u = (pair.Item3 - pair.Item1) / pair.Item2;
			var v = (pair.Item4 - pair.Item1) / pair.Item2;
			low = MathF.Max( low, MathF.Min( u, v ) );
			high = MathF.Min( high, MathF.Max( u, v ) );
			if ( low > high )
				return false;
		}

		return true;
	}

	static bool TriangleHit( Vector3 a, Vector3 b, Triangle triangle )
	{
		var ray = b - a;
		var e1 = triangle.B - triangle.A;
		var e2 = triangle.C - triangle.A;
		var p = Vector3.Cross( ray, e2 );
		var det = e1.Dot( p );
		if ( MathF.Abs( det ) < .000001f )
			return false;
		var inverse = 1 / det;
		var offset = a - triangle.A;
		var u = offset.Dot( p ) * inverse;
		if ( u < -.0001f || u > 1.0001f )
			return false;
		var q = Vector3.Cross( offset, e1 );
		var v = ray.Dot( q ) * inverse;
		if ( v < -.0001f || u + v > 1.0001f )
			return false;
		var t = e2.Dot( q ) * inverse;
		return t > .0001f && t < .9999f;
	}

	/// <summary>Nine connector rays through the planned assembly. This is a bounded obstruction screen, not an exact swept-volume solver.</summary>
	public static bool Blocked( IReadOnlyList<Obstacle> obstacles, SurfaceBuildContext surface, AnchorSite site, Vector3 end, float width )
	{
		var start = surface.World.PointToWorld( site.Position );
		var delta = end - start;
		if ( delta.Length < .001f )
			return false;
		var rotation = Rotation.LookAt( delta.Normal, MathF.Abs( delta.Normal.Dot( Vector3.Up ) ) > .99f ? Vector3.Left : Vector3.Up );
		foreach ( var x in new[]
		{
			-.5f,
			0,
			.5f
		}

 )
			foreach ( var y in new[]
			{
				-.5f,
				0,
				.5f
			}

 )
			{
				var offset = (rotation.Left * x + rotation.Up * y) * width;
				var a = surface.World.PointToLocal( start + offset );
				var b = surface.World.PointToLocal( end + offset );
				foreach ( var obstacle in obstacles )
				{
					if ( site.AttachmentParts.Contains( obstacle.Id ) || !BoxHit( a, b, obstacle.Bounds ) )
						continue;
					if ( !obstacle.Complete || obstacle.Triangles.Any( t => TriangleHit( a, b, t ) ) )
						return true;
				}
			}

		return false;
	}

	/// <summary>Exact separating-axis test for the two rectangular mounting plate boxes in world space.</summary>
	public static bool PlatesOverlap( AnchorResult a, AnchorResult b, AnchorOptions options )
	{
		var axesA = new[]
		{
			a.PlateFrame.Rotation.Forward,
			a.PlateFrame.Rotation.Left,
			a.PlateFrame.Rotation.Up
		};
		var axesB = new[]
		{
			b.PlateFrame.Rotation.Forward,
			b.PlateFrame.Rotation.Left,
			b.PlateFrame.Rotation.Up
		};
		var half = new[]
		{
			options.Width * .5f,
			options.Height * .5f,
			options.Thickness * .5f
		};
		var centerA = a.PlateFrame.PointToWorld( Vector3.Up * half[2] );
		var centerB = b.PlateFrame.PointToWorld( Vector3.Up * half[2] );
		var axes = axesA.Concat( axesB ).Concat( axesA.SelectMany( x => axesB.Select( y => Vector3.Cross( x, y ) ) ) );
		foreach ( var raw in axes )
		{
			if ( raw.Length < .00001f )
				continue;
			var axis = raw.Normal;
			var reachA = 0f;
			var reachB = 0f;
			for ( var i = 0; i < 3; i++ )
			{
				reachA += MathF.Abs( axis.Dot( axesA[i] ) ) * half[i];
				reachB += MathF.Abs( axis.Dot( axesB[i] ) ) * half[i];
			}

			if ( MathF.Abs( (centerB - centerA).Dot( axis ) ) >= reachA + reachB - .02f )
				return false;
		}

		return true;
	}
}