editor/AdvancedMappingTools/Framework/AnchorSolver.cs

Editor-side mounting solver for the Advanced Mapping Tools generator. Probes surfaces with raycasts, validates plate footprint and clearance, then emits plate, bolt, collar and connector geometry into a part plan. Also tracks per-owner mount status and which settings to hide.

namespace AdvancedMappingTools;
public sealed class AnchorOptions
{
	public float Range { get; set; } = 256;
	public float Minimum { get; set; } = .01f;
	public float Width { get; set; } = 12;
	public float Height { get; set; } = 16;
	public float Thickness { get; set; } = 1;
	public float Clearance { get; set; }
	public float Rotation { get; set; }
	public bool Bolts { get; set; } = true;
	public float BoltRadius { get; set; } = .65f;
	public bool Connector { get; set; } = true;
	public float ConnectorWidth { get; set; } = 2;
	public bool CheckEdges { get; set; } = true;
	public string IncludeTags { get; set; } = "";
	public string ExcludeTags { get; set; } = "";
	public string Material { get; set; }
	public bool Valid => new[]
	{
		Range,
		Minimum,
		Width,
		Height,
		Thickness,
		Clearance,
		Rotation
	}.All( float.IsFinite ) && Minimum >= 0 && Range > Minimum && Range <= 100000 && Width >= 1 && Width <= 512 && Height >= 1 && Height <= 512 && Thickness >= .1f && Thickness <= 64 && Clearance >= 0 && Clearance <= 128 && MathF.Abs( Rotation ) <= 360 && (!Bolts || float.IsFinite( BoltRadius ) && BoltRadius >= .1f && BoltRadius <= MathF.Min( Width, Height ) * .12f) && (!Connector || float.IsFinite( ConnectorWidth ) && ConnectorWidth >= .1f && ConnectorWidth <= MathF.Min( Width, Height ) * .5f);
}

public sealed record AnchorSite( string Id, Vector3 Position, Vector3 Direction )
{
	public AnchorCollar Collar { get; init; }
	public string[] AttachmentParts { get; init; } = [];
}

public sealed record AnchorResult( AnchorSite Site, SurfaceSample Surface, Transform PlateFrame, Vector3 ConnectorEnd, bool Mounted, string Message );
public interface IAnchorAwareSettings
{
	bool MountsEnabled { get; }

	bool ShowMountDebug { get; }

	AnchorOptions MountOptions();
	IEnumerable<AnchorSite> MountSites();
	string MountOwner { get; }
}

/// <summary>World-space mounting solution; plates are generated only after measured surface contact.</summary>
public static class AnchorSolver
{
	internal static void Reject( SurfaceReadSession read, SurfaceSample sample, string message )
	{
		for ( var i = read.Samples.Count - 1; i >= 0; i-- )
			if ( ReferenceEquals( read.Samples[i], sample ) )
			{
				read.Samples[i] = sample with
				{
					Status = SurfaceStatus.Rejected,
					Message = message
				};
				break;
			}
	}

	public static AnchorResult Solve( SurfaceBuildContext context, AnchorSite site, AnchorOptions options )
	{
		AnchorResult Fail( string message, SurfaceSample sample = null ) => new( site, sample, Transform.Zero, Vector3.Zero, false, message );
		if ( context == null )
			return Fail( "No scene context; no mount generated." );
		if ( options == null || !options.Valid || !SurfaceSolver.Finite( site.Position ) || !SurfaceSolver.Finite( site.Direction ) || site.Direction.Length < .001f )
			return Fail( "Invalid mounting options or site." );
		var source = context.World.PointToWorld( site.Position );
		var direction = (context.World.PointToWorld( site.Position + site.Direction ) - source).Normal;
		var sample = context.Read.Probe( source, direction, new() { MinimumDistance = options.Minimum, MaximumDistance = options.Range, IncludeTags = options.IncludeTags, ExcludeTags = options.ExcludeTags } );
		if ( !sample.Usable )
			return Fail( sample.Message, sample );
		var normal = sample.Normal;
		var rotation = SurfaceSolver.AlignToNormal( Rotation.Identity, normal ) * Rotation.FromYaw( options.Rotation );
		var frame = new Transform( sample.Position + normal * options.Clearance, rotation );
		var end = frame.PointToWorld( Vector3.Up * options.Thickness );
		if ( normal.Dot( -direction ) < .1f || (source - end).Dot( normal ) < .01f )
		{
			Reject( context.Read, sample, "Plate would cross the attachment point." );
			return Fail( "Insufficient clearance for mounting plate.", sample );
		}

		if ( options.CheckEdges )
		{
			foreach ( var xy in new[]
			{
				new Vector3( -options.Width * .5f, -options.Height * .5f, 0 ),
				new Vector3( options.Width * .5f, -options.Height * .5f, 0 ),
				new Vector3( options.Width * .5f, options.Height * .5f, 0 ),
				new Vector3( -options.Width * .5f, options.Height * .5f, 0 )
			}

 )
			{
				var point = sample.Position + rotation * xy;
				var corner = context.Read.Probe( point + normal * 2, -normal, new() { MinimumDistance = .01f, MaximumDistance = 4, IncludeTags = options.IncludeTags, ExcludeTags = options.ExcludeTags } );
				if ( !corner.Usable || corner.Normal.Dot( normal ) < .98f || corner.Position.Distance( point ) > .25f )
				{
					Reject( context.Read, sample, "Plate footprint is not supported by the surface." );
					return Fail( "Plate extends beyond a continuous mounting surface.", sample );
				}
			}
		}

		return new( site, sample, frame, end, true, "Measured mount ready." );
	}

	public static void Add( PartPlan root, GenContext context, SurfaceBuildContext surface, AnchorResult result, AnchorOptions options )
	{
		if ( !result.Mounted )
			return;
		var material = context.Material( options.Material );
		var prefix = "mount/" + result.Site.Id;
		void Part( string suffix, string label, Action<MeshKit> draw )
		{
			var worldKit = new MeshKit
			{
				Material = material,
				LiftFlushFaces = false
			};
			draw( worldKit );
			var mesh = worldKit.Finish();
			foreach ( var vertex in mesh.VertexHandles.ToArray() )
				mesh.SetVertexPosition( vertex, surface.World.PointToLocal( mesh.GetVertexPosition( vertex ) ) );
			root.Add( context.Solid( prefix + suffix, label, "SurfaceMount", "mount", material, k =>
			{
				k.LiftFlushFaces = false;
				k.ImportMesh( mesh, Transform.Zero );
			} ) );
		}

		Part( "/plate", "Measured Mounting Plate", k =>
		{
			k.Frame = result.PlateFrame;
			k.Box( new( -options.Width * .5f, -options.Height * .5f, 0 ), new( options.Width * .5f, options.Height * .5f, options.Thickness ) );
		} );
		if ( options.Bolts )
			for ( var i = 0; i < 4; i++ )
			{
				var index = i;
				Part( "/bolt" + i, "Mount Bolt", k =>
				{
					k.Frame = result.PlateFrame;
					var x = (index % 2 == 0 ? -1 : 1) * options.Width * .32f;
					var y = (index < 2 ? -1 : 1) * options.Height * .32f;
					k.Cylinder( new( x, y, 0 ), options.BoltRadius, options.BoltRadius, options.Thickness, options.Thickness + options.BoltRadius, 6 );
				} );
			}

		if ( result.Site.Collar is { } collar )
			Part( "/collar", "Route Mount Clamp", k =>
			{
				Vector3 P( int i, bool outer, float z ) => surface.World.PointToWorld( collar.Point( i, outer, z ) );
				var low = -collar.Width * .5f;
				var high = collar.Width * .5f;
				k.BeginPart();
				for ( var i = 0; i < collar.Sides; i++ )
				{
					var j = (i + 1) % collar.Sides;
					k.Face( P( i, true, low ), P( j, true, low ), P( j, true, high ), P( i, true, high ) );
					k.Face( P( j, false, low ), P( i, false, low ), P( i, false, high ), P( j, false, high ) );
					k.Face( P( i, false, high ), P( i, true, high ), P( j, true, high ), P( j, false, high ) );
					k.Face( P( j, false, low ), P( j, true, low ), P( i, true, low ), P( i, false, low ) );
				}
			} );
		if ( options.Connector )
			Part( "/connector", "Measured Mount Connector", k => k.Beam( surface.World.PointToWorld( result.Site.Position ), result.ConnectorEnd, options.ConnectorWidth, options.ConnectorWidth ) );
	}
}

public static class AnchorIntegration
{
	public static void Append( PartPlan root, GeneratorSettings settings, GenContext context )
	{
		if ( settings is IAnchorAwareSettings aware )
			Append( root, aware, context );
	}

	public static void Append( PartPlan root, IAnchorAwareSettings aware, GenContext context )
	{
		if ( !aware.MountsEnabled )
			return;
		if ( context.Surface == null )
		{
			SurfaceDebug.Note( aware.MountOwner, "No scene context; no surface mounts generated." );
			MountStatus[aware.MountOwner] = "No scene context; no mounts generated.";
			return;
		}

		var surface = context.Surface with
		{
			World = context.Surface.World.ToWorld( root.Local )
		};
		var options = aware.MountOptions();
		var mounted = 0;
		var missing = 0;
		string last = "";
		var sites = aware.MountSites().Take( 257 ).ToArray();
		if ( sites.Length == 0 )
			last = "No eligible mounting points; check route, direction, spacing and clearance around fittings.";
		var acceptedPlates = new List<AnchorResult>();
		var obstacles = aware is IRouteAnchorSettings { MountAvoidAssembly: true } ? RouteMounting.Obstacles( root ) : null;
		foreach ( var site in sites.Take( 256 ) )
		{
			var result = AnchorSolver.Solve( surface, site, options );
			if ( result.Mounted && options.Connector && obstacles != null && RouteMounting.Blocked( obstacles, surface, site, result.ConnectorEnd, options.ConnectorWidth ) )
			{
				missing++;
				last = "Connector is blocked by another route part.";
				AnchorSolver.Reject( surface.Read, result.Surface, last );
				continue;
			}

			if ( result.Mounted && aware is IRouteAnchorSettings { MountAvoidAssembly: true } && acceptedPlates.Any( other => RouteMounting.PlatesOverlap( result, other, options ) ) )
			{
				missing++;
				last = "Mounting plate overlaps another plate in this route.";
				AnchorSolver.Reject( surface.Read, result.Surface, last );
				continue;
			}

			if ( result.Mounted )
			{
				acceptedPlates.Add( result );
				AnchorSolver.Add( root, context, surface, result, options );
				mounted++;
			}
			else
			{
				missing++;
				last = result.Message;
			}
		}

		if ( sites.Length > 256 )
			last += " Mount limit reached: only the first 256 sites are generated.";
		SurfaceDebug.Record( aware.MountOwner, surface.Read );
		MountStatus[aware.MountOwner] = $"{mounted} mounted; {missing} missing. {last}";
	}

	static readonly Dictionary<string, string> MountStatus = [];
	public static string Status( string owner ) => MountStatus.TryGetValue( owner, out var status ) ? status : "No mounts measured yet.";
	public static HashSet<string> Hidden( IAnchorAwareSettings settings )
	{
		var hidden = new HashSet<string>();
		if ( !settings.MountsEnabled )
			hidden.UnionWith( ["MountRange", "MountMinimum", "MountWidth", "MountHeight", "MountThickness", "MountClearance", "MountRotation", "MountInset", "MountBolts", "MountBoltRadius", "MountConnector", "MountConnectorWidth", "MountCheckEdges", "MountIncludeTags", "MountExcludeTags", "MountMaterial", "ShowMountDebug"] );
		var options = settings.MountOptions();
		if ( !options.Bolts )
			hidden.Add( "MountBoltRadius" );
		if ( !options.Connector )
			hidden.Add( "MountConnectorWidth" );
		return hidden;
	}
}