Editor utility for generating beam geometry in the architecture system. Builds mesh or plank fills for beams, computes plank specs, applies pillar dressing to beam parts, and reports lane count based on beam geometry.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A beam is boards, so it goes through ArchPlanks - the one board emitter - rather than laying its own.
// One member is the same shape as a slab of the same footprint, so that goes through ArchFloorGen.
public static class ArchBeamGen
{
public static void Build(
ArchMesh canvas,
ArchBeamPart beam,
ArchRoom room,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
ArchStyle style )
{
var outline = beam.Outline();
if ( outline.Count < 3 )
{
return;
}
var chain = new[] { beam.Palette, room.Palette, building.Palette };
var brush = style.Brush( ArchSurface.Deck, chain );
var region = new[] { (IReadOnlyList<Vector2>)outline };
// STANDING as itself: a carve authored before this timber existed leaves it whole, and one authored
// over it takes it. The stack decides, not the geometry.
var (wells, recesses) = ArchFloorGen.Bites( plan, beam.Level, kit, beam.Soffit, beam.TopHeight, building.Id, ArchCutAffects.Beams, beam.Id );
// EXACTLY the loop that was dragged - box, circle or whatever the form gave - at exactly the band it
// was dragged at. A single member is not divided, snapped or sectioned by anything.
if ( beam.Members == BeamMembers.One )
{
ArchFloorGen.Slab( canvas, outline, wells, beam.Soffit, beam.TopHeight, brush, recesses );
return;
}
ArchPlanks.Fill( canvas, region, wells, Spec( beam ), brush );
}
// Lanes run unbroken: a stagger is a floor's joint pattern, and a slat that breaks halfway across a
// ceiling reads as two slats end to end. So the run length is the whole diagonal. The section comes off
// the pillar type the field was dressed from, which is why nothing here re-derives one.
static ArchPlankSpec Spec( ArchBeamPart beam )
{
return new ArchPlankSpec
{
Width = MathF.Max( 1f, beam.MemberWidth ),
Gap = MathF.Max( 0f, beam.MemberGap ),
Thickness = beam.Depth,
Length = (beam.Max - beam.Min).Length + beam.Depth,
Top = beam.TopHeight,
Yaw = beam.Yaw,
Soffit = true,
Sides = beam.Sides
};
}
// A slat is a pillar lying down, so the authored types ARE its options - but only its SECTION carries
// over. A pillar's bay is columns feet apart and reads as three slats in a whole ceiling, so the rhythm
// comes off the section instead: bar and space alike, and how many of them from the drag.
public static ArchBeamPart Dressed( ArchBeamPart beam, ArchPillarType type )
{
if ( type?.Column is not { } column )
{
return beam;
}
beam.Type = type.Name;
beam.Sides = column.Sides;
beam.MemberWidth = MathF.Max( 1f, column.Width );
beam.MemberGap = MathF.Max( 1f, column.Width );
return beam;
}
// What the field actually lays, read back by the ghost and the report so neither counts its own lanes.
public static int Lanes( ArchBeamPart beam )
{
if ( beam.Members == BeamMembers.One )
{
return 1;
}
var pitch = MathF.Max( 1f, beam.MemberWidth ) + MathF.Max( 0f, beam.MemberGap );
var facing = Rotation.FromYaw( beam.Yaw );
var across = new Vector2( -facing.Forward.y, facing.Forward.x );
var outline = beam.Outline();
var spread = outline.Select( point => Vector2.Dot( point, across ) ).ToList();
return ArchDivide.AtMost( spread.Max() - spread.Min(), pitch ).Count;
}
}