Utility for handling raked (ramped) parts in the editor. Defines the IArchRamped interface and static helpers to compute whether a part rakes, ramp fall clamping, run/climb directions, and to produce an ArchCarvePlane for a ramped deck based on a platform loop, yaw and rise.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A part whose band has a RAKED end. A platform rakes its top and a cut rakes the floor it leaves behind, and
// those are the same three numbers - so they are asked for through one interface and answered in one place
// rather than a slope being derived twice with the two drifting a degree apart.
public interface IArchRamped
{
bool Ramp { get; set; }
float RampFall { get; set; }
float RampYaw { get; set; }
}
// ONE resolution, read by the ghost, the generator, the handles and the report - a preview that derived its own
// slope would show a wedge the rebuild laid flat.
public static class ArchRamp
{
public static bool Rakes( IArchRamped part ) => part is { Ramp: true };
// Zero is the WHOLE rise, the way a wall modifier's zero height is its wall's own head: a ramp left alone
// runs from the ground to its top, and pulling the head keeps it doing that instead of leaving a shelf.
public static float Fall( IArchRamped part, float rise )
{
var authored = part.RampFall > ArchGridService.FinestSize ? part.RampFall : rise;
return Math.Clamp( authored, 0f, rise );
}
public static float Fall( ArchPlatformPart platform ) => Fall( platform, platform.Rise );
public static float Foot( ArchPlatformPart platform )
{
return Rakes( platform ) ? platform.TopHeight - Fall( platform ) : platform.TopHeight;
}
public static Vector2 Climb( float yaw )
{
var radians = yaw.DegreeToRadian();
return new Vector2( MathF.Cos( radians ), MathF.Sin( radians ) );
}
public static ArchCarvePlane Deck( ArchPlatformPart platform )
{
return Deck( platform, platform.Outline(), platform.TopHeight, platform.Rise );
}
// The raked plane of any ramped part, off its own band. A part that does not rake answers with the level
// plane at the same head, so a caller never has to branch on it.
public static ArchCarvePlane Deck( IArchRamped part, IReadOnlyList<Vector2> loop, float top, float rise )
{
return Rakes( part )
? Deck( loop, top, Fall( part, rise ), part.RampYaw )
: ArchCarvePlane.Level( top );
}
// Anchored at the loop's HIGH extreme, so the head is exactly the top that was dragged and the foot falls
// away from it. Anchored at the centre instead, pulling the head would carry both ends up with it.
public static ArchCarvePlane Deck( IReadOnlyList<Vector2> loop, float top, float fall, float yaw )
{
var climb = Climb( yaw );
var run = Run( loop, climb );
if ( run < ArchGridService.FinestSize || fall < ArchGridService.FinestSize )
{
return ArchCarvePlane.Level( top );
}
return ArchCarvePlane.Through( Head( loop, climb ), top, climb * (fall / run) );
}
// How far the footprint reaches along the climb - the run the fall is spread over. A gradient is never
// authored, so a ramp dragged longer is a gentler one with no second edit.
public static float Run( IReadOnlyList<Vector2> loop, Vector2 climb )
{
if ( loop is not { Count: >= 3 } )
{
return 0f;
}
var reach = loop.Select( point => Vector2.Dot( point, climb ) ).ToList();
return reach.Max() - reach.Min();
}
// The way a raked top climbs when a DRAG names it: squared to the axis the gesture mostly ran along,
// because a box drawn corner to corner means a ramp along that box and not a deck falling on the diagonal.
public static float Climbed( Vector2 from, Vector2 to )
{
var span = to - from;
if ( MathF.Abs( span.x ) >= MathF.Abs( span.y ) )
{
return span.x < 0f ? 180f : 0f;
}
return span.y < 0f ? 270f : 90f;
}
static Vector2 Head( IReadOnlyList<Vector2> loop, Vector2 climb )
{
return loop.OrderByDescending( point => Vector2.Dot( point, climb ) ).First();
}
}