Editor/Roof/ArchRoofLight.cs

Editor utility for roof lights in the architecture editor. It finds roof parts under or over a point, enumerates roof sections for a plan/level, tests whether a point lies in a roof outline, and creates a new ArchRoofLightPart with basic validation and initialization.

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

namespace Sunless.Architecture;

// Deliberately permissive: the module that lofts the deck does the clipping, so a ridge drag becomes a light on
// each slope. Roof plant - a chimney, a vent stack, a tank, a hatch - is placed through here too, because it is the same
// rectangle dragged on the same deck; what each form builds is ArchRoofLightKinds' answer, not a second path.
public static class ArchRoofLight
{
	public const float MinSize = 12f;

	// Every building on the storey, never the active one - its placeholder lookup reports nothing there. The FORM
	// decides which sections count: ArchRoofPlane.Carries is the one answer, and a dormer is carried by a hip where
	// a light is not.
	public static ArchRoofPart Under( ArchPlan plan, int level, Vector2 point, RoofLightForm form = RoofLightForm.Kerbed )
	{
		return Under( plan?.Buildings, level, point, form );
	}

	// Handed the buildings, because a recipe stands a monitor on a building not yet filed in the plan.
	public static ArchRoofPart Under( IEnumerable<ArchBuilding> buildings, int level, Vector2 point, RoofLightForm form = RoofLightForm.Kerbed )
	{
		return Sections( buildings, level ).FirstOrDefault( roof => ArchRoofPlane.Carries( roof, form ) && Holds( roof, point ) );
	}

	// Planar or not, so a refusal can name the style it found.
	public static ArchRoofPart Over( ArchPlan plan, int level, Vector2 point )
	{
		return Sections( plan?.Buildings, level ).FirstOrDefault( roof => Holds( roof, point ) );
	}

	public static IEnumerable<ArchRoofPart> Sections( ArchPlan plan, int level ) => Sections( plan?.Buildings, level );

	static IEnumerable<ArchRoofPart> Sections( IEnumerable<ArchBuilding> buildings, int level )
	{
		if ( buildings is null )
		{
			return Enumerable.Empty<ArchRoofPart>();
		}

		var kinds = ArchKinds.Load();

		return buildings
			.SelectMany( building => ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds ) )
			.OfType<ArchRoofPart>()
			.Where( roof => roof.Level == level );
	}

	static bool Holds( ArchRoofPart roof, Vector2 point )
	{
		return ArchFootprint.Encloses( new[] { (IReadOnlyList<Vector2>)roof.Outline() }, point );
	}


	// No clamping: Lit clips to covered patches, so half-on stays half-on.
	public static ArchRoofLightPart Add( ArchPlan plan, ArchRoofPart roof, Vector2 from, Vector2 to, ArchRoofLightPart draft )
	{
		if ( roof is null )
		{
			return null;
		}

		var min = new Vector2( MathF.Min( from.x, to.x ), MathF.Min( from.y, to.y ) );
		var max = new Vector2( MathF.Max( from.x, to.x ), MathF.Max( from.y, to.y ) );

		if ( max.x - min.x < MinSize || max.y - min.y < MinSize )
		{
			return null;
		}

		var light = new ArchRoofLightPart
		{
			Id = plan.AllocateId(),
			Name = $"{draft.Form.Tag()}{roof.Lights.Count + 1}",
			Form = draft.Form,
			Min = min,
			Max = max,
			Curb = draft.Curb,
			CurbWidth = draft.CurbWidth,
			Glazed = draft.Glazed,
			PaneSpan = draft.PaneSpan
		};

		roof.Lights.Add( light );

		return light;
	}
}