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;
	}
}