Editor procedural generator that builds a steel frame along a path. Places columns at bay intervals, a beam along the top of each segment, and optional base plates, anchor bolts, cross/V/single diagonal bracing and knee braces, each as its own part.
namespace AdvancedMappingTools;
public enum FrameBrace
{
[Title( "None" )] None,
[Title( "Cross Braces" )] Cross,
[Title( "V Braces" )] V,
[Title( "Single Diagonal" )] Single
}
public sealed class SteelFrameSettings : GeneratorSettings
{
/// <summary>
/// The line the frame follows, set by the tool.
/// </summary>
public PathCurve Path { get; set; } = PathCurve.FromPoints( [new Vector3( 0, 0, 0 ), new Vector3( 512, 0, 0 )] );
[Property, Group( "Frame" ), Range( 16, 2048, slider: false ), WideMode, Description( "Height of the columns, measured from the path." )]
public float Height { get; set; } = 160.0f;
[Property, Group( "Frame" ), Title( "Bay Spacing" ), Range( 16, 2048, slider: false ), WideMode, Description( "Distance between two columns." )]
public float BaySpacing { get; set; } = 128.0f;
[Property, Group( "Frame" ), Title( "Column Size" ), Range( 1, 64, slider: false ), WideMode]
public float ColumnSize { get; set; } = 8.0f;
[Property, Group( "Frame" ), Title( "Beam Width" ), Range( 1, 64, slider: false ), WideMode]
public float BeamWidth { get; set; } = 6.0f;
[Property, Group( "Frame" ), Title( "Beam Depth" ), Range( 1, 128, slider: false ), WideMode]
public float BeamDepth { get; set; } = 12.0f;
[Property, Group( "Frame" ), Title( "Braces" ), WideMode, Description( "Diagonal bracing between the columns of a bay." )]
public FrameBrace Braces { get; set; } = FrameBrace.None;
[Property, Group( "Frame" ), Title( "Base Plates" ), WideMode]
public bool BasePlates { get; set; } = true;
[Property, ShowIf( nameof( BasePlates ), true ), Group( "Frame" ), Title( "Base Plate Size" ), Range( 4, 128, slider: false ), WideMode]
public float BasePlateSize { get; set; } = 24.0f;
[Property, Group( "Frame" ), Title( "Knee Braces" ), WideMode, Description( "Short diagonals between every column and the beam." )]
public bool KneeBraces { get; set; } = false;
[Property, ShowIf( nameof( KneeBraces ), true ), Group( "Frame" ), Title( "Knee Length" ), Range( 4, 256, slider: false ), WideMode]
public float KneeLength { get; set; } = 32.0f;
[Property, ShowIf( nameof( BasePlates ), true ), Group( "Frame" ), Title( "Base Bolts" ), WideMode, Description( "Four anchor bolts on every base plate." )]
public bool BaseBolts { get; set; } = false;
[Property, HideIf( nameof( Braces ), FrameBrace.None ), Group( "Frame" ), Title( "Brace Every" ), Range( 1, 8, slider: false ), WideMode, Description( "How many bays share one set of bracing. One braces every bay." )]
public int BraceEvery { get; set; } = 1;
[Property, Group( "Materials" ), Title( "Steel" ), ResourceType( "vmat" ), WideMode]
public string SteelMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Base" ), ResourceType( "vmat" ), WideMode]
public string BaseMaterial { get; set; }
}
/// <summary>
/// A steel frame along a path: a column at every bay, a beam along the top, optional base
/// plates and diagonal bracing. Every column, beam and brace is a part of its own, so a
/// frame can be edited afterwards like any other building.
/// </summary>
public sealed class SteelFrameGenerator : IPartGenerator
{
const string FrameCategory = "frame";
const string BraceCategory = "brace";
const string BaseCategory = "base";
const float PlateThickness = 1.5f;
public string Id => "steelframe";
public string Title => "Steel Frame";
public Type SettingsType => typeof( SteelFrameSettings );
public IReadOnlyList<string> SeedNames => [FrameCategory, BraceCategory, BaseCategory];
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (SteelFrameSettings)settings;
var root = context.Group( "root", "Steel Frame", "Building", FrameCategory );
if ( s.Path is null || s.Path.Nodes.Count < 2 ) return root;
var points = new List<Vector3>();
foreach ( var node in s.Path.Nodes )
{
var point = node.Position;
if ( points.Count == 0 || points[^1].Distance( point ) > 0.5f ) points.Add( point );
}
if ( points.Count < 2 ) return root;
var steel = context.Material( s.SteelMaterial );
var baseMaterial = context.Material( s.BaseMaterial );
var height = MathF.Max( s.Height, 1.0f );
var column = MathF.Max( s.ColumnSize, 0.5f );
var half = column * 0.5f;
var columns = new List<Vector3>();
var columnIndex = 0;
void ColumnAt( Vector3 point )
{
foreach ( var existing in columns )
{
if ( existing.Distance( point ) < 0.5f ) return;
}
columns.Add( point );
var at = point;
var id = $"column{columnIndex:00}";
// On a base plate the column starts on top of it: two bottoms on the ground would flicker.
var foot = s.BasePlates ? PlateThickness : 0.0f;
root.Add( context.Solid( id, $"Column {columnIndex + 1}", "FrameColumn", FrameCategory, steel,
kit => kit.Box( new Vector3( at.x - half, at.y - half, at.z + foot ), new Vector3( at.x + half, at.y + half, at.z + height ) ) ) );
if ( s.BasePlates )
{
var plate = MathF.Max( s.BasePlateSize, 2.0f ) * 0.5f;
var baseId = $"base{columnIndex:00}";
root.Add( context.Solid( baseId, $"Base Plate {columnIndex + 1}", "FrameBase", BaseCategory, baseMaterial,
kit => kit.Box( new Vector3( at.x - plate, at.y - plate, at.z ), new Vector3( at.x + plate, at.y + plate, at.z + PlateThickness ) ) ) );
if ( s.BaseBolts )
{
var bolt = MathF.Min( 2.0f, MathF.Max( plate * 0.15f, 0.4f ) );
var spread = plate * 0.62f;
var spot = at;
root.Add( context.Solid( $"bolts{columnIndex:00}", $"Base Bolts {columnIndex + 1}", "FrameBolt", BaseCategory, baseMaterial,
kit =>
{
foreach ( var (dx, dy) in new[] { (-1.0f, -1.0f), (1.0f, -1.0f), (1.0f, 1.0f), (-1.0f, 1.0f) } )
{
kit.Box( new Vector3( spot.x + dx * spread - bolt, spot.y + dy * spread - bolt, spot.z + PlateThickness ),
new Vector3( spot.x + dx * spread + bolt, spot.y + dy * spread + bolt, spot.z + PlateThickness + bolt * 3.0f ) );
}
} ) );
}
}
columnIndex++;
}
var braceIndex = 0;
void Brace( Vector3 from, Vector3 to )
{
if ( s.Braces == FrameBrace.None ) return;
var size = MathF.Max( column * 0.4f, 0.5f );
var top = Vector3.Up * height;
var id = $"brace{braceIndex:00}";
if ( s.Braces == FrameBrace.Cross )
{
root.Add( context.Solid( id, $"Brace {braceIndex + 1}", "FrameBrace", BraceCategory, steel,
kit => kit.Beam( from, to + top, size, size ) ) );
braceIndex++;
root.Add( context.Solid( $"brace{braceIndex:00}", $"Brace {braceIndex + 1}", "FrameBrace", BraceCategory, steel,
kit => kit.Beam( from + top, to, size, size ) ) );
}
else if ( s.Braces == FrameBrace.V )
{
var middle = (from + to) * 0.5f;
root.Add( context.Solid( id, $"Brace {braceIndex + 1}", "FrameBrace", BraceCategory, steel,
kit => kit.Beam( middle, from + top, size, size ) ) );
braceIndex++;
root.Add( context.Solid( $"brace{braceIndex:00}", $"Brace {braceIndex + 1}", "FrameBrace", BraceCategory, steel,
kit => kit.Beam( middle, to + top, size, size ) ) );
}
else
{
root.Add( context.Solid( id, $"Brace {braceIndex + 1}", "FrameBrace", BraceCategory, steel,
kit => kit.Beam( from, to + top, size, size ) ) );
}
braceIndex++;
}
var kneeIndex = 0;
void Knee( Vector3 from, Vector3 to )
{
if ( !s.KneeBraces ) return;
var size = MathF.Max( column * 0.35f, 0.5f );
var id = $"knee{kneeIndex:00}";
root.Add( context.Solid( id, $"Knee Brace {kneeIndex + 1}", "FrameKnee", BraceCategory, steel,
kit => kit.Beam( from, to, size, size ) ) );
kneeIndex++;
}
for ( var i = 0; i < points.Count - 1; i++ )
{
var a = points[i];
var b = points[i + 1];
var length = a.Distance( b );
if ( length < 0.01f ) continue;
var bays = Math.Max( 1, (int)MathF.Round( length / MathF.Max( s.BaySpacing, 8.0f ) ) );
var bayLength = length / bays;
var direction = (b - a).Normal;
var knee = MathF.Min( MathF.Max( s.KneeLength, 4.0f ), MathF.Min( height - 1.0f, bayLength * 0.45f ) );
for ( var k = 0; k < bays; k++ )
{
var bayStart = Vector3.Lerp( a, b, k / (float)bays );
var bayEnd = Vector3.Lerp( a, b, (k + 1) / (float)bays );
ColumnAt( bayStart );
if ( k % Math.Max( s.BraceEvery, 1 ) == 0 )
Brace( bayStart, bayEnd );
if ( knee > 1.0f && height > knee + 1.0f )
{
Knee( bayStart + Vector3.Up * (height - knee), bayStart + Vector3.Up * height + direction * knee );
Knee( bayEnd + Vector3.Up * (height - knee), bayEnd + Vector3.Up * height - direction * knee );
}
}
ColumnAt( b );
var start = a;
var end = b;
root.Add( context.Solid( $"beam{i:00}", $"Beam {i + 1}", "FrameBeam", FrameCategory, steel,
kit => kit.Beam( start + Vector3.Up * height, end + Vector3.Up * height, s.BeamWidth, s.BeamDepth ) ) );
}
return root;
}
}