Editor utility that builds precast barrier geometry for an architectural fence/wall system. It computes post cross-sections, plinths, brackets, droppers (hanging wires) and emits geometry into an ArchMesh using provided specs, kit params and brushes.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Bays are LEVEL - a cast panel cannot rake - so the wall staircases down a slope.
public static class ArchBarrierPrecast
{
public static void Post( ArchMesh canvas, ArchBarrierPost post, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes, float bottom, float top )
{
var along = ArchBarrierGen.Along( post.Frame );
var across = ArchBarrierGen.Across( post.Frame );
var lean = across * post.Lean;
var section = Section( spec, kit );
var lower = new List<Vector3>();
var upper = new List<Vector3>();
foreach ( var point in section )
{
var offset = along * point.x + across * point.y;
lower.Add( new Vector3( post.Flat.x + offset.x, post.Flat.y + offset.y, bottom ) );
upper.Add( new Vector3( post.Flat.x + offset.x + lean.x, post.Flat.y + offset.y + lean.y, top ) );
}
canvas.Prism( lower, upper, brushes.Post );
Bracket( canvas, post, spec, kit, brushes, top );
}
// H in plan, slot opening along the run: how a slab is dropped in.
static List<Vector2> Section( ArchBarrierSpec spec, ArchKit kit )
{
var depth = MathF.Max( 3f, kit.PrecastPostDepth ) * 0.5f;
var width = MathF.Max( 3f, spec.PostSize > 2f ? spec.PostSize : kit.PrecastPostWidth ) * 0.5f;
var slot = Math.Clamp( kit.PrecastSlotDepth, 0.4f, depth - 0.6f );
// Groove a bite wider than the slab, or both faces land coplanar over the seat.
var half = MathF.Max( 0.4f, spec.PanelThickness ) * 0.5f + ArchContact.Bite( kit );
return new List<Vector2>
{
new( -depth, -width ),
new( -depth, -half ),
new( -depth + slot, -half ),
new( -depth + slot, half ),
new( -depth, half ),
new( -depth, width ),
new( depth, width ),
new( depth, half ),
new( depth - slot, half ),
new( depth - slot, -half ),
new( depth, -half ),
new( depth, -width )
};
}
public static void Bay( ArchMesh canvas, ArchBarrierBayShape bay, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes, float inset )
{
var depth = MathF.Max( 3f, kit.PrecastPostDepth ) * 0.5f;
var slot = Math.Clamp( kit.PrecastSlotDepth, 0.4f, depth - 0.6f );
var seat = bay.SeatFrom;
// Slab seats a bite past the groove floor, or joints show daylight or coplanar caps.
var reach = depth - slot - ArchContact.Bite( kit );
var from = bay.From.Flat + bay.Along * reach;
var to = bay.To.Flat - bay.Along * reach;
if ( (to - from).Length < 4f )
{
return;
}
Plinth( canvas, bay, spec, kit, brushes, from, to, seat );
var half = MathF.Max( 0.4f, spec.PanelThickness ) * 0.5f;
var course = spec.Course;
var joint = MathF.Max( 0f, kit.PrecastJoint );
var standing = Standing( bay, spec );
// The height line is a ceiling, not a target: nothing stands over it.
var ceiling = seat + spec.Height;
for ( var index = 0; index < standing; index++ )
{
var slip = index == standing - 1 ? bay.Slip : bay.Slip * 0.35f;
var lift = seat + MathF.Max( 0f, spec.PlinthHeight ) + index * course + slip;
var top = MathF.Min( lift + course - joint, ceiling );
if ( top - lift < 0.5f )
{
break;
}
var lean = bay.Lean * (index + 1) / MathF.Max( 1, standing );
canvas.Rake( from, to, -half + lean, half + lean, lift, lift, top, top, brushes.Panel );
}
Droppers( canvas, bay, spec, kit, brushes );
}
// A slab wall fails from the TOP down, never the whole bay at once.
static int Standing( ArchBarrierBayShape bay, ArchBarrierSpec spec )
{
var courses = Math.Max( 1, bay.Courses );
if ( !bay.Missing )
{
return courses;
}
var lost = 1 + (int)MathF.Floor( ArchBarrierShape.Noise( spec.Seed, bay.Index, 83 ) * courses );
return Math.Max( 0, courses - lost );
}
// Carried down to the ground, absorbing the step between two level bays.
static void Plinth( ArchMesh canvas, ArchBarrierBayShape bay, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes, Vector2 from, Vector2 to, float seat )
{
var top = seat + MathF.Max( 0f, spec.PlinthHeight );
var proud = MathF.Max( 0f, kit.PrecastPlinthProud );
var half = MathF.Max( 0.4f, spec.PanelThickness ) * 0.5f + proud;
var bottomFrom = MathF.Min( bay.From.Frame.Position.z, top ) - 6f;
var bottomTo = MathF.Min( bay.To.Frame.Position.z, top ) - 6f;
if ( top - MathF.Max( bottomFrom, bottomTo ) < 0.5f )
{
return;
}
canvas.Rake( from, to, -half, half, bottomFrom, bottomTo, top, top, brushes.Plinth );
}
// Emitted with the post, or a shared post gets two brackets inside each other.
static void Bracket( ArchMesh canvas, ArchBarrierPost post, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes, float top )
{
var thickness = MathF.Max( 0.3f, kit.DropperThickness ) * 0.5f;
for ( var bracket = 0; bracket < Math.Clamp( spec.Droppers, 0, 2 ); bracket++ )
{
var foot = Foot( post, top );
Bar( canvas, foot, foot + Rise( post, spec, Side( spec, bracket ) ), thickness, brushes.Post );
}
}
// Run from this bay's brackets, so the fence follows the wall down a step.
static void Droppers( ArchMesh canvas, ArchBarrierBayShape bay, ArchBarrierSpec spec, ArchKit kit, ArchBarrierBrushes brushes )
{
var brackets = Math.Clamp( spec.Droppers, 0, 2 );
var strands = Math.Clamp( spec.Strands, 0, 12 );
if ( brackets == 0 || strands == 0 )
{
return;
}
var wire = ArchBarrierShape.WireHalf( kit );
var overhang = MathF.Max( 0f, spec.PostOverhang );
for ( var bracket = 0; bracket < brackets; bracket++ )
{
var side = Side( spec, bracket );
var footFrom = Foot( bay.From, bay.SeatFrom + spec.Height + overhang );
var footTo = Foot( bay.To, bay.SeatTo + spec.Height + overhang );
var headFrom = footFrom + Rise( bay.From, spec, side );
var headTo = footTo + Rise( bay.To, spec, side );
for ( var strand = 0; strand < strands; strand++ )
{
if ( bay.Missing && ArchBarrierShape.Noise( spec.Seed, bay.Index * 31 + strand, 97 ) < spec.Broken )
{
continue;
}
var fraction = (strand + 1) / (float)strands;
var sag = spec.Wear * bay.Span * 0.012f * ArchBarrierShape.Noise( spec.Seed, bay.Index * 17 + strand, 103 );
Bar( canvas,
Vector3.Lerp( footFrom, headFrom, fraction ) - Vector3.Up * sag,
Vector3.Lerp( footTo, headTo, fraction ) - Vector3.Up * sag,
wire, brushes.Wire );
}
}
}
static float Side( ArchBarrierSpec spec, int bracket )
{
return spec.Droppers == 1 || bracket == 0 ? 1f : -1f;
}
static Vector3 Foot( ArchBarrierPost post, float height )
{
return new Vector3( post.Flat.x, post.Flat.y, height );
}
static Vector3 Rise( ArchBarrierPost post, ArchBarrierSpec spec, float side )
{
var reach = MathF.Max( 4f, spec.DropperLength );
var lean = Math.Clamp( spec.DropperLean, 0f, 75f ).DegreeToRadian();
var across = ArchBarrierGen.Across( post.Frame ) * (MathF.Sin( lean ) * reach * side);
return new Vector3( across.x, across.y, MathF.Cos( lean ) * reach );
}
// Oriented off the run, not world axes, so dropper and sloped wire share a shape.
static void Bar( ArchMesh canvas, Vector3 from, Vector3 to, float half, ArchBrush brush )
{
var span = to - from;
if ( span.Length < 0.5f )
{
return;
}
var along = span.Normal;
var reference = MathF.Abs( along.z ) > 0.98f ? Vector3.Forward : Vector3.Up;
var right = Vector3.Cross( along, reference ).Normal;
var up = Vector3.Cross( right, along ).Normal;
var section = new[]
{
-right * half - up * half,
right * half - up * half,
right * half + up * half,
-right * half + up * half
};
var near = new List<Vector3>();
var far = new List<Vector3>();
foreach ( var offset in section )
{
near.Add( from + offset );
far.Add( to + offset );
}
canvas.Prism( near, far, brush );
}
}