Editor/Beam/ArchBeamGen.cs
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;
}
}