Editor helper for placing and resolving architectural ladders. Defines ArchLadderShape with geometry helpers and static ArchLadder methods to place a ladder on a plan, compute its heights, rungs/brackets/hoops, and top/head/grade based on building, kit and plan data.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly struct ArchLadderShape
{
public Vector2 Anchor { get; init; }
public Vector2 Facing { get; init; }
public Vector2 Along { get; init; }
public float Foot { get; init; }
public float Head { get; init; }
public float Bar { get; init; }
public float Back { get; init; }
public float Front { get; init; }
public float HalfWidth { get; init; }
public List<float> Rungs { get; init; }
public List<float> Brackets { get; init; }
public List<float> Hoops { get; init; }
public float CageReach { get; init; }
public float ReturnRise { get; init; }
public float ReturnReach { get; init; }
public Vector2 Stile( float side ) => Anchor + Along * (HalfWidth * side);
public float Top => Head + ReturnRise;
public List<Vector2> Hoop()
{
var left = Stile( -1f );
var right = Stile( 1f );
return new List<Vector2>
{
left + Facing * Front,
left + Facing * CageReach,
right + Facing * CageReach,
right + Facing * Front
};
}
}
public static class ArchLadder
{
// Every building, because ActiveBuilding falls back to the empty placeholder.
public static ArchLadderPart Place( ArchPlan plan, ArchKit kit, Vector2 point, ArchLadderPart draft, out ArchBuilding host )
{
return Place( plan?.Buildings, plan, kit, point, draft, out host );
}
// Handed the buildings, because a recipe hangs a ladder on a building not yet filed in the plan.
public static ArchLadderPart Place( IEnumerable<ArchBuilding> buildings, ArchPlan plan, ArchKit kit, Vector2 point, ArchLadderPart draft, out ArchBuilding host )
{
host = null;
if ( buildings is null || plan is null || draft is null )
{
return null;
}
// Level null, unlike a balcony or a fire escape: a ladder climbs the whole elevation and resolves its
// top from whatever deck, parapet or wall plate it reaches, so it is not scoped to one storey.
var station = ArchFixtureAnchor.At( buildings, null, new ArchGridService(), point, draft.Width, plan );
return Place( plan, kit, station, draft, out host );
}
public static ArchLadderPart Place( ArchPlan plan, ArchKit kit, ArchFixtureStation? at, ArchLadderPart draft, out ArchBuilding host )
{
host = null;
if ( plan is null || draft is null || at is not { } station )
{
return null;
}
host = station.Building;
var centre = station.Centre;
var thickness = station.Wall.Thickness > 0f ? station.Wall.Thickness : kit.WallThickness;
var inside = centre - station.Outward * (thickness * 0.5f + 1f);
return new ArchLadderPart
{
Id = plan.AllocateId(),
Name = $"Ladder{plan.CountFiled( ArchKind.Ladder, host ) + 1}",
Anchor = centre + station.Outward * (thickness * 0.5f),
Outward = station.Outward,
BaseHeight = Grade( host, kit, plan ),
TopHeight = Head( plan, host, station.Room, inside ),
Width = station.Width,
Standoff = draft.Standoff,
RungSpacing = draft.RungSpacing,
Cage = draft.Cage,
CageFrom = draft.CageFrom,
Return = draft.Return
};
}
public static ArchLadderShape Resolve( ArchLadderPart ladder, ArchKit kit )
{
var bar = MathF.Max( 0.5f, kit.LadderBar );
var back = MathF.Max( bar, ladder.Standoff );
var rise = ladder.Rise;
var cageFrom = Math.Clamp( ladder.CageFrom, 0f, rise );
return new ArchLadderShape
{
Anchor = ladder.Anchor,
Facing = ladder.Facing,
Along = ladder.Along,
Foot = ladder.BaseHeight,
Head = ladder.TopHeight,
Bar = bar,
Back = back,
Front = back + bar,
HalfWidth = MathF.Max( bar * 2f, ladder.Width * 0.5f ),
// Foot node dropped so the last rung lands on the head, not a part-pitch short.
Rungs = Heights( ladder.BaseHeight, ArchDivide.AtMost( rise, MathF.Max( 6f, ladder.RungSpacing ) ).Nodes.Skip( 1 ) ),
Brackets = Heights( ladder.BaseHeight, ArchDivide.AtMost( rise, kit.LadderBracketSpacing ).Nodes ),
Hoops = ladder.Cage
? Heights( ladder.BaseHeight + cageFrom, ArchDivide.AtMost( rise - cageFrom, kit.LadderCageSpacing ).Nodes )
: new List<float>(),
CageReach = MathF.Max( back + bar * 2f, kit.LadderCageReach ),
ReturnRise = ladder.Return ? kit.HandrailHeight : 0f,
ReturnReach = ladder.Return ? back + bar + kit.WallThickness + 12f : 0f
};
}
static List<float> Heights( float from, IEnumerable<float> offsets )
{
return offsets.Select( offset => from + offset ).ToList();
}
// Built in the building's lifted space: grade sits a plinth below the ground floor.
static float Grade( ArchBuilding building, ArchKit kit, ArchPlan plan )
{
var lowest = float.MaxValue;
foreach ( var room in building.Rooms )
{
lowest = MathF.Min( lowest, room.BaseHeight );
}
return (lowest == float.MaxValue ? 0f : lowest) - ArchAsks.Lift( plan, building, kit );
}
static float Head( ArchPlan plan, ArchBuilding building, ArchRoom room, Vector2 inside )
{
var head = float.MinValue;
foreach ( var roof in plan.Filed( ArchKind.Roof, building ).OfType<ArchRoofPart>() )
{
if ( !ArchFootprint.Encloses( new[] { (IReadOnlyList<Vector2>)roof.Outline() }, inside ) )
{
continue;
}
var deck = roof.BaseHeight + roof.Thickness + (roof.Parapet ? MathF.Max( 0f, roof.ParapetHeight ) : 0f);
head = MathF.Max( head, deck );
}
return head > float.MinValue ? head : (room?.BaseHeight ?? 0f) + (room?.WallHeight ?? 0f);
}
}