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