Editor/Roof/ArchRoofLight.cs
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 ) => Owned( plan, level ).Select( entry => entry.Roof );
// Each section with the building that OWNS it, for a caller measuring against geometry rather than against the
// plan: what stands in the scene rides its building's grade lift, and only the owner can say how far.
public static IEnumerable<(ArchBuilding On, ArchRoofPart Roof)> Owned( ArchPlan plan, int level ) {
if ( plan?.Buildings is null ) {
yield break;
}
var kinds = ArchKinds.Load();
foreach ( var building in plan.Buildings ) {
foreach ( var roof in ArchPlanStore.FiledOn( ArchKind.Roof, building, kinds ).OfType<ArchRoofPart>() ) {
if ( roof.Level == level ) {
yield return (building, roof);
}
}
}
}
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;
}
}