Editor/Pillar/ArchPillarHandler.cs
namespace Sunless.Architecture;

public sealed class ArchPillarHandler : IArchHandler {
	public ArchKind Kind => ArchKind.Pillar;

	public bool Draw( ArchTool tool, ArchSelection picked ) {
		if ( picked.Item is not ArchPillarPart pillar ) {
			return false;
		}

		var gesture = ArchGesture.On( ArchKind.Pillar, pillar.Id );

		// The part is authored on the flat plan and poured onto the building's grade, so a widget drawn at the
		// authored height stands a foundation below the column it is meant to be holding. The RESOLVED foot, so
		// the widget is on the column the author can see rather than on the seat it was placed with.
		var lift = picked.Lift( tool.Plan, tool.Kit );
		var foot = ArchPillarGen.Seat( pillar, tool.Kit, tool.Plan );
		var seat = new Vector3( pillar.Origin.x, pillar.Origin.y, foot + lift );

		if ( ArchShapeHandles.Position( tool, gesture.At( "move" ), seat, out var travelled ) ) {
			pillar.Origin = new Vector2( travelled.x, travelled.y );
			pillar.BaseHeight = travelled.z - lift;

			// A drag that only slid the column across the plan leaves it seeking whatever is under it there; one
			// that lifted it off its deck is an authored foot, or the next rebuild would drop it straight back.
			pillar.Founded = pillar.Founded || MathF.Abs( travelled.z - seat.z ) > ArchGridService.FinestSize;

			return true;
		}

		if ( ArchShapeHandles.Turned( gesture.At( "turn" ), seat, out var spun ) ) {
			pillar.Yaw = ArchShapeHandles.Facing( pillar.Yaw + spun );

			return true;
		}

		return Pulled( tool, pillar, gesture, foot, lift );
	}

	// The section, the grid's reach and the rise in ONE gesture: the six faces of the box the part occupies.
	// A lone column takes a pulled face as its section and a grid takes it as reach re-divided at the bay it
	// already stands, so the box always means the same thing - the bounds of what is there.
	static bool Pulled( ArchTool tool, ArchPillarPart pillar, ArchGesture gesture, float foot, float lift ) {
		var frame = Rotation.FromYaw( pillar.Yaw );
		var (min, max) = Local( pillar );
		var floor = foot + lift;
		var head = floor + ArchPillarGen.Height( pillar, tool.Plan.RoomStanding( pillar ), tool.Kit, tool.Plan );

		if ( !ArchShapeHandles.Volume( tool, gesture.At( "volume" ), min, max, floor, head, out var pulled, frame ) ) {
			return false;
		}

		var alongX = Divided( tool.Grid, pulled.Mins.x, pulled.Maxs.x, pillar.CountX, pillar.Spacing.x, pillar.Width );
		var alongY = Divided( tool.Grid, pulled.Mins.y, pulled.Maxs.y, pillar.CountY, pillar.Spacing.y, pillar.Depth );
		var origin = frame * new Vector3( alongX.Origin, alongY.Origin, 0f );

		// A height already authored keeps being authored; one still reaching for its soffit is only pinned by a
		// drag that moved the head, or pulling a side would quietly stop the column tracking what covers it.
		var pinned = pillar.Height > 0f || MathF.Abs( pulled.Maxs.z - head ) > ArchGridService.FinestSize;

		pillar.Origin = new Vector2( origin.x, origin.y );
		pillar.Width = alongX.Section;
		pillar.Depth = alongY.Section;
		pillar.CountX = alongX.Count;
		pillar.CountY = alongY.Count;
		pillar.Spacing = new Vector2( alongX.Spacing, alongY.Spacing );
		pillar.BaseHeight = pulled.Mins.z - lift;
		pillar.Founded = pillar.Founded || MathF.Abs( pulled.Mins.z - floor ) > ArchGridService.FinestSize;

		if ( pinned ) {
			pillar.Height = MathF.Max( 8f, pulled.Maxs.z - pulled.Mins.z );
		}

		return true;
	}

	// The part's bounds with the yaw taken out of them - the columns march on the grid's own axes whichever way
	// the part is turned to face, and a signed bay runs the grid back towards its origin.
	static (Vector2 Min, Vector2 Max) Local( ArchPillarPart pillar ) {
		var turned = Rotation.FromYaw( -pillar.Yaw ) * new Vector3( pillar.Origin.x, pillar.Origin.y, 0f );
		var seat = new Vector2( turned.x, turned.y );
		var far = seat + new Vector2(
			(Math.Max( 1, pillar.CountX ) - 1) * pillar.Spacing.x,
			(Math.Max( 1, pillar.CountY ) - 1) * pillar.Spacing.y );
		var half = pillar.Half;

		return (Vector2.Min( seat, far ) - half, Vector2.Max( seat, far ) + half);
	}

	readonly record struct Extent( float Origin, int Count, float Spacing, float Section );

	// One column has no bays to divide, so its whole span IS its section. More than one keeps the section it was
	// dressed with and spends the rest of the span on bays, which is what makes a colonnade grow by columns
	// rather than by stretching the ones it has.
	static Extent Divided( ArchGridService grid, float low, float high, int count, float spacing, float section ) {
		var span = MathF.Max( ArchGridService.FinestSize, high - low );

		if ( count <= 1 ) {
			return new Extent( (low + high) * 0.5f, 1, spacing, span );
		}

		var kept = MathF.Min( section, span );
		var bays = grid.DivideAtMost( span - kept, MathF.Max( 32f, MathF.Abs( spacing ) ) );

		return new Extent( low + kept * 0.5f, bays.Count + 1, bays.Step, kept );
	}
}