Editor tool that places cable routes along a drawn path. Generates cable geometry with sag, bundle layouts, corner clamps, end collars and bundle ties, and builds a preview mesh.
namespace AdvancedMappingTools;
public enum CableBundleLayout
{
Row,
Round
}
public sealed partial class CableRouteSettings : GeneratorSettings
{
public override IReadOnlySet<string> UnusedOptions
{
get
{
var h = new HashSet<string>
{
nameof( Variation )
};
if ( !Sag )
h.Add( nameof( SagDepth ) );
if ( !Sag || Cables == 1 || CableRouteGeometry.Tied( this ) )
h.Add( nameof( SagVariation ) );
if ( Cables == 1 )
h.UnionWith( [nameof( BundleLayout ), nameof( TieSpacing )] );
if ( Cables == 1 )
h.Add( nameof( CableGap ) );
h.UnionWith( RouteMounting.Hidden( this ) );
return h;
}
}
public PathCurve Path { get; set; } = PathCurve.FromPoints( [new Vector3( 0, 0, 128 ), new Vector3( 256, 0, 128 )] );
[Property, Group( "Cable" ), Range( .25f, 32, slider: false ), WideMode]
public float Radius { get; set; } = 2;
[Property, Group( "Cable" ), Range( 1, 12, slider: false ), WideMode]
public int Cables { get; set; } = 1;
[Property, Group( "Cable" ), Range( 0, 32, slider: false ), WideMode]
public float CableGap { get; set; } = 2;
[Property, Group( "Sag" ), WideMode]
public bool Sag { get; set; } = true;
[Property, Group( "Sag" ), Range( 0, 256, slider: false ), WideMode]
public float SagDepth { get; set; } = 24;
[Property, Group( "Detail" ), Range( 4, 32, slider: false ), WideMode]
public int Sides { get; set; } = 12;
[Property, Group( "Detail" ), Range( 4, 128, slider: false ), WideMode]
public float SampleStep { get; set; } = 16;
[Property, Group( "Supports" ), WideMode]
public bool EndCollars { get; set; } = true;
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string CableMaterial { get; set; }
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string CollarMaterial { get; set; }
/// <summary>How much the sag of neighbouring cables differs, so a bundle does not look cloned.</summary>
[Property, Group( "Sag" ), Title( "Sag Variation" ), Range( 0, 1, slider: false ), WideMode]
public float SagVariation { get; set; } = .15f;
/// <summary>A clamp on every cable where the route turns at a point.</summary>
[Property, Group( "Supports" ), Title( "Corner Clamps" ), WideMode]
public bool CornerClamps { get; set; } = true;
/// <summary>Row lays the cables side by side; Round packs three or more in a circle.</summary>
[Property, Group( "Cable" ), Title( "Bundle Layout" ), WideMode]
public CableBundleLayout BundleLayout { get; set; } = CableBundleLayout.Row;
/// <summary>A tie around all cables every this many units; below 8 means no ties.</summary>
[Property, Group( "Supports" ), Title( "Tie Spacing" ), Range( 0, 512, slider: false ), WideMode]
public float TieSpacing { get; set; }
}
public sealed class CableRouteGenerator : IPartGenerator
{
public string Id => "cable_route";
public string Title => "Cable Route";
public Type SettingsType => typeof(CableRouteSettings);
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (CableRouteSettings)settings;
var root = context.Group( "root", Title, "Building", "cable" );
if ( s.Path is null || s.Path.SegmentCount == 0 )
return root;
var radius = Math.Clamp( s.Radius, .25f, 32 );
var sides = Math.Clamp( s.Sides, 4, 32 );
var outline = Enumerable.Range( 0, sides ).Select( i => new Vector2( MathF.Cos( i * MathF.PI * 2 / sides ), MathF.Sin( i * MathF.PI * 2 / sides ) ) * radius ).ToArray();
foreach ( var route in CableRouteGeometry.Tracks( s ) )
{
var cable = route.Index;
var track = route.Track;
root.Add( context.Solid( $"cable{cable}", "Cable", "Cable", "cable", context.Material( s.CableMaterial ), kit =>
{
if ( !s.Path.Closed )
SweepEngine.Build( kit, track, 0, track.Count - 1, outline );
else
{
var rings = outline.Select( p => Enumerable.Range( 0, track.Count - 1 ).Select( i => track.Place( i, p.x, p.y ) ).ToArray() ).ToArray();
kit.Sweep( rings, true );
}
} ) );
if ( s.CornerClamps )
foreach ( var corner in route.Corners.Where( i => (s.Path.Closed || i > 0) && i < track.Count - 1 ) )
{
var f = track.Frames[corner];
root.Add( context.Solid( $"clamp{cable}_{corner}", "Corner Clamp", "CableCollar", "cable", context.Material( s.CollarMaterial ?? s.CableMaterial ), kit =>
{
var half = MathF.Max( radius * 1.2f, 1 );
kit.Beam( f.Position - f.Forward * half, f.Position + f.Forward * half, radius * 2.6f, radius * 2.6f );
} ) );
}
if ( s.EndCollars && !s.Path.Closed )
foreach ( var end in new[]
{
0,
track.Count - 1
}
)
{
var f = track.Frames[end];
root.Add( context.Solid( $"collar{cable}_{end}", "End Collar", "CableCollar", "cable", context.Material( s.CollarMaterial ?? s.CableMaterial ), kit =>
{
var axis = end == 0 ? f.Forward : -f.Forward;
kit.Beam( f.Position - axis * 2, f.Position, radius * 2.8f, radius * 2.8f );
} ) );
}
}
Ties( root, s, radius, context );
return root;
}
/// <summary>Hollow bands around the whole bundle: four thin bars that touch the outer cables.</summary>
static void Ties( PartPlan root, CableRouteSettings s, float radius, GenContext context )
{
if ( Math.Clamp( s.Cables, 1, 12 ) < 2 || !float.IsFinite( s.TieSpacing ) || s.TieSpacing < 8 )
return;
var center = CableRouteGeometry.Center( s );
if ( center is null )
return;
var spacing = MathF.Min( s.TieSpacing, 512 );
var length = center.Frames[^1].Distance;
var tracks = CableRouteGeometry.Tracks( s ).Select( t => t.Track.Frames.Select( f => f.Position ).ToArray() ).ToArray();
var bar = MathF.Max( .3f, radius * .25f );
var width = MathF.Max( 1, radius * .8f );
var index = 0;
for ( var d = spacing; d < length - spacing * .25f; d += spacing )
{
var f = center.FrameAt( d );
var up = MathF.Abs( f.Forward.z ) > .99f ? Vector3.Forward : Vector3.Up;
var frame = new Transform( f.Position, Rotation.LookAt( f.Forward, up ) );
// Measure where each cable crosses the tie's plane; near bends the cables spread out.
var hx = 0.0f;
var hy = 0.0f;
var found = true;
foreach ( var points in tracks )
{
var best = float.MaxValue;
var hit = Vector3.Zero;
var slant = 1.0f;
for ( var i = 0; i < points.Length - 1; i++ )
{
var a = (points[i] - f.Position).Dot( f.Forward );
var b = (points[i + 1] - f.Position).Dot( f.Forward );
if ( a * b > 0 || MathF.Abs( a - b ) < 1e-5f )
continue;
var p = Vector3.Lerp( points[i], points[i + 1], a / (a - b) );
if ( p.Distance( f.Position ) >= best )
continue;
best = p.Distance( f.Position );
hit = p;
slant = MathF.Max( MathF.Abs( (points[i + 1] - points[i]).Normal.Dot( f.Forward ) ), .5f );
}
if ( best == float.MaxValue )
{
found = false;
break;
}
var local = frame.PointToLocal( hit );
hx = MathF.Max( hx, MathF.Abs( local.y ) + radius / slant + width * .5f * MathF.Sqrt( 1 - slant * slant ) / slant );
hy = MathF.Max( hy, MathF.Abs( local.z ) + radius / slant + width * .5f * MathF.Sqrt( 1 - slant * slant ) / slant );
}
if ( !found )
continue;
hx += .02f;
hy += .02f;
root.Add( context.Solid( $"tie{index++}", "Cable Tie", "CableCollar", "cable", context.Material( s.CollarMaterial ?? s.CableMaterial ), kit =>
{
kit.Frame = frame;
kit.Box( new( -width * .5f, -hx - bar, hy ), new( width * .5f, hx + bar, hy + bar ) );
kit.Box( new( -width * .5f, -hx - bar, -hy - bar ), new( width * .5f, hx + bar, -hy ) );
kit.Box( new( -width * .5f, hx, -hy ), new( width * .5f, hx + bar, hy ) );
kit.Box( new( -width * .5f, -hx - bar, -hy ), new( width * .5f, -hx, hy ) );
} ) );
}
}
}
[ToolCategory( "Industrial" ), Title( "Cable Route" ), Icon( "cable" )]
public sealed class CableRouteEditor( PrimitiveTool tool ) : PathEditor( tool )
{
public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string>
{
"CurveSegments",
"Variation",
"Smooth"
};
readonly CableRouteGenerator generator = new();
public CableRouteSettings Settings { get; } = new();
public override object ExtraSettings => Settings;
protected override int MinPoints => 2;
protected override bool AllowsHeight => true;
protected override bool CanClose => true;
protected override bool UsesBends => false;
protected override bool PlacesObjects => true;
public override bool ChangesOtherSettings( string property ) => property is nameof( CableRouteSettings.MountsEnabled ) or nameof( CableRouteSettings.MountCollars ) or nameof( CableRouteSettings.MountBolts ) or nameof( CableRouteSettings.MountConnector ) or nameof( CableRouteSettings.Sag ) or nameof( CableRouteSettings.Cables ) or nameof( CableRouteSettings.BundleLayout ) or nameof( CableRouteSettings.TieSpacing );
protected override void BuildMesh( MeshKit kit )
{
}
void Prepare()
{
Settings.Path = Curve();
Settings.CableMaterial = PartTool.Pick( Settings.CableMaterial, SurfaceMaterial );
Settings.CollarMaterial = PartTool.Pick( Settings.CollarMaterial, TrimMaterial );
}
protected override PolygonMesh BuildPreview()
{
Prepare();
return PartService.Plan( generator, Settings, surface: SurfaceIntegration.Context( Settings, SceneEditorSession.Active?.Scene, Transform.Zero ) ).Flatten( Transform.Zero );
}
public override void OnUpdate( SceneTrace trace )
{
base.OnUpdate( trace );
if ( Settings.MountsEnabled && Settings.ShowMountDebug )
SurfaceDebug.Draw( Settings.MountOwner );
}
protected override void PlaceObjects()
{
Prepare();
PartTool.Place( generator, Settings, Transform.Zero );
}
}