Editor/Stair/ArchStairCarve.cs

Editor utility for carving straight stair flights from a drag input. It sketches a stair part from a drag, assigns IDs/names and files carved stairs to a plan, resolves stair geometry against a building, and enumerates solid volumes left by the carve.

File AccessNetworking
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Nothing here emits a tread — every step is host material that survived the cut.
public static class ArchStairCarve
{
	public const float MinRun = 18f;

	public static ArchStairPart Cut( ArchPlan plan, ArchCarveDrag drag, ArchKit kit, ArchStairPart template )
	{
		var flight = Sketch( drag, kit, template );

		if ( flight is null )
		{
			return null;
		}

		var kinds = ArchKinds.Load();

		flight.Id = plan.AllocateId();
		flight.Name = $"Steps{plan.CountFiled( ArchKind.CarvedStair, drag.Host, kinds ) + 1}";

		plan.File( ArchKind.CarvedStair, drag.Host, flight, kinds );

		return flight;
	}

	// One drag description feeds ghost and cut, so the notch previewed is the notch carved.
	public static ArchStairPart Sketch( ArchCarveDrag drag, ArchKit kit, ArchStairPart template )
	{
		if ( drag is not { Lands: true } )
		{
			return null;
		}

		var host = drag.Host;
		var from = drag.Foot;
		var to = drag.Head;
		var span = to - from;

		// A square drag keeps its box reading — the perpendicular extent is the width.
		var yaw = ArchGridService.Snap( MathF.Atan2( span.y, span.x ).RadianToDegree(), ArchGridService.AngleStep );
		var axes = new ArchStairAxes { Yaw = yaw };
		var square = MathF.Abs( yaw % 90f ) < 0.01f;
		var reach = Vector2.Dot( span, axes.Across );
		var length = MathF.Abs( Vector2.Dot( span, axes.Along ) );

		if ( length < MinRun )
		{
			return null;
		}

		var authored = template?.Width > 1f ? template.Width : kit.StairWidth;
		var clear = square && MathF.Abs( reach ) >= MinRun ? MathF.Abs( reach ) : MathF.Max( MinRun, authored );
		var rise = host.Rise();
		var riser = template?.StepRise > 0.5f ? template.StepRise : kit.StepRise;
		var risers = Math.Max( 1, (int)MathF.Ceiling( rise / MathF.Max( 3f, riser ) ) );
		var foot = from + axes.Across * (reach * 0.5f);

		// The drag IS the flight; a carve never turns, so the shaft holds exactly one lane.
		var origin = foot - axes.Across * (clear * 0.5f);
		var (core, lanes) = ArchStairCore.Straight( origin, axes.Along, length, clear, rise );

		return new ArchStairPart
		{
			BaseHeight = host.GradeHeight,
			Core = core,
			Lanes = lanes,
			Width = clear,
			// The host's rise is fixed, so the riser bends — arriving under the coping is a trip.
			StepRise = rise / risers,
			// And the going bends with it: the resolve tiles a run into its DRAWN length, so a going
			// left at the kit's would cut however many treads fit the drag rather than the climb.
			StepGoing = length / risers,
			Guard = template?.Guard ?? StairGuard.None,
			// A carved flight arrives on the host's own surface, so there is nothing for a landing to be.
			TopLanding = false
		};
	}

	// The one place a carved flight resolves — ghost, generator and report read the same steps.
	public static ArchStairShape Resolve( IArchCarveHost host, ArchStairPart stair, ArchBuilding building, ArchKit kit )
	{
		var storey = building?.Rooms?.Where( room => room.Floor == host.Level ) ?? Enumerable.Empty<ArchRoom>();

		return ArchStairShape.Resolve( stair, null, kit, storey );
	}

	// What is left standing under the stepped profile IS the flight.
	public static IEnumerable<ArchCarveVolume> Volumes( IArchCarveHost host, ArchStairPart stair, ArchBuilding building, ArchKit kit )
	{
		var shape = Resolve( host, stair, building, kit );

		foreach ( var run in shape.Runs )
		{
			for ( var step = 0; step < run.Steps; step++ )
			{
				var footprint = run.Axes.Rect( step * run.Going, (step + 1) * run.Going, 0f, run.Width );

				yield return ArchCarveVolume.Over( footprint, run.StepTop( step ), host.TopHeight );
			}
		}

		// A landing is one very wide step — runs alone leave a turn's corner solid.
		foreach ( var pad in shape.Pads )
		{
			yield return ArchCarveVolume.Over( pad.Loop(), pad.Height, host.TopHeight );
		}
	}

}