Editor utility that generates approaches (steps and ramps) for architectural building parts. It computes embeddings, reach, builds step flights, straight ramps, spline ramps, kerbs and outputs mesh geometry via ArchMesh and related builders.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Steps and ramps share one rise: both climb from grade to the same floor.
public static class ArchApproachGen
{
// Two bites: one clears the plinth's inner face, one buries the deck's end.
static float Embed( ArchKit kit ) => Reach( kit ) + ArchContact.Bite( kit ) * 2f;
// The plinth face to the footprint line, which is where the floor slab starts.
static float Reach( ArchKit kit ) => MathF.Max( 0f, kit.FoundationOversize ) + MathF.Max( 0f, kit.WallThickness ) * 0.5f;
// Only has to straddle the footprint line the plinth is built round.
const float MouthDepth = 3f;
// Narrower than the approach by a bite, so cut ends stay buried in the deck.
// Subtract callers pass the full course depth; the default only straddles the line.
public static ArchFloorCutout Mouth( ArchApproachPart approach, ArchKit kit, float reach = MouthDepth )
{
var outward = ArchApproachAxes.Outward( approach );
var across = ArchApproachAxes.Across( approach );
var half = ArchApproachAxes.HalfWidth( approach ) - ArchContact.Bite( kit );
var centre = approach.Origin - outward * Reach( kit );
var garden = MathF.Max( MouthDepth, reach );
var mouth = new ArchFloorCutout { Id = approach.Id, Name = approach.Name, OwnerId = approach.Id };
mouth.Reshape( new[]
{
centre + across * half - outward * MouthDepth,
centre + across * half + outward * garden,
centre - across * half + outward * garden,
centre - across * half - outward * MouthDepth
} );
return mouth;
}
public static void Build(
ArchMesh canvas,
ArchApproachPart approach,
ArchRoom room,
ArchBuilding building,
ArchKit kit,
ArchStyle style,
List<ArchDoorRequest> doors,
ArchPlan plan = null )
{
var rise = Rise( room, building, kit, plan );
if ( rise < 2f )
{
return;
}
if ( approach.Kind == ApproachKind.Steps )
{
Steps( canvas, approach, room, building, kit, style, doors, rise, plan );
return;
}
if ( approach.IsSpline )
{
SplineRamp( canvas, approach, room, building, kit, style );
return;
}
Ramp( canvas, approach, room, building, kit, style, plan, rise );
}
// Grade sits BELOW zero in plan space by the whole plinth lift.
public static float Rise( ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null )
{
return room.BaseHeight + ArchFloorGen.GradeLift( plan, building, kit );
}
static float Grade( ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null )
{
return room.BaseHeight - Rise( room, building, kit, plan );
}
// Synthesised as a real flight so treads, risers, stringers and rails come from the generator.
static void Steps(
ArchMesh canvas,
ArchApproachPart approach,
ArchRoom room,
ArchBuilding building,
ArchKit kit,
ArchStyle style,
List<ArchDoorRequest> doors,
float rise,
ArchPlan plan )
{
var outward = ArchApproachAxes.Outward( approach );
var across = ArchApproachAxes.Across( approach );
// Travels INWARD: origin is the run's bottom, across 0 its right-hand edge at the door.
var flight = new ArchStairPart
{
Id = approach.Id,
Name = approach.Name,
BaseHeight = Grade( room, building, kit, plan ),
Core = new ArchStairCore { Rise = MathF.Max( 1f, rise ) },
Width = ArchApproachAxes.HalfWidth( approach ) * 2f,
StepRise = MathF.Max( 3f, kit.StepRise ),
TopLanding = false,
// No well guard: the approach climbs from outside, never through a floor.
WellGuard = false,
Guard = approach.Rails ? StairGuard.Both : StairGuard.None,
TreadThickness = 2.5f,
Palette = approach.Palette
};
// Count comes off the flight itself: recounting ran a step short on some rises.
var count = Math.Max( 1, flight.StepCount );
var going = approach.Run > 1f ? approach.Run / count : MathF.Max( 6f, kit.StepGoing );
flight.StepGoing = going;
// Buried into the wall: a top tread ending on the plinth face caps in mid-air.
var origin = approach.Origin + outward * (count * going - Embed( kit )) + across * (flight.Width * 0.5f);
// Travels inward, off the plinth face toward the door.
var (core, lanes) = ArchStairCore.Straight( origin, -outward, count * going, flight.Width, rise );
flight.Core = core;
flight.Lanes = lanes;
ArchFlightBuilders.Build( new ArchFlightDrawing( canvas, flight, room, building, kit, style, doors, plan ) );
}
// One resolution for generator, terrain stamp and report, so grading matches the built drive.
public readonly struct ArchRampRun
{
public Vector2 Wall { get; init; }
public Vector2 Foot { get; init; }
public float WallTop { get; init; }
public float FootTop { get; init; }
public float Grade { get; init; }
public float HalfWidth { get; init; }
}
public static bool Resolved( ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan, out ArchRampRun ramp )
{
ramp = default;
var rise = Rise( room, building, kit, plan );
if ( approach.Kind != ApproachKind.Ramp || rise < 2f )
{
return false;
}
var outward = ArchApproachAxes.Outward( approach );
var grade = Grade( room, building, kit, plan );
var run = approach.Run > 1f ? approach.Run : rise / Math.Clamp( kit.RampSlope, 0.02f, 1f );
var land = grade;
// A drive reaching the road clamps to the pavement's back, one gradient with the crossover.
if ( ArchAnswers.Load().Asks<IArchApproachLanding>() is { } landing
&& landing.TryLand( plan, approach, kit, run, out var reach, out var height ) )
{
run = reach;
land = grade + height;
}
ramp = new ArchRampRun
{
Wall = approach.Origin - outward * Embed( kit ),
Foot = approach.Origin + outward * run,
WallTop = room.BaseHeight,
FootTop = land,
Grade = grade,
HalfWidth = ArchApproachAxes.HalfWidth( approach )
};
return true;
}
// Outer end keeps a lip: a face closing to zero height silently fails to build.
static void Ramp( ArchMesh canvas, ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style, ArchPlan plan, float rise )
{
if ( !Resolved( approach, room, building, kit, plan, out var ramp ) )
{
return;
}
var chain = new[] { approach.Palette, room.Palette, building.Palette };
var brush = style.Brush( ArchSurface.Deck, chain );
var across = ArchApproachAxes.Across( approach );
var half = ramp.HalfWidth;
var top = ramp.WallTop;
var land = ramp.FootTop;
// LEVEL underside, under grade at both ends: the drive is a slab on fill.
var bottom = MathF.Min( ramp.Grade, land ) - MathF.Max( 1f, kit.RampThickness );
Wedge( canvas, ramp.Wall, ramp.Foot, across, half, -half, bottom, bottom, top, land, brush );
if ( !approach.Kerbs )
{
return;
}
var kerb = MathF.Max( 1f, kit.RampKerbWidth );
var stand = MathF.Max( 1f, kit.RampKerbHeight );
var bite = ArchContact.Bite( kit );
var trim = style.Brush( ArchSurface.Baseboard, chain );
// Held a hair INSIDE the deck's edge: flush puts two vertical faces on one plane.
Wedge( canvas, ramp.Wall, ramp.Foot, across, half - bite, half - kerb, top - bite, land - bite, top + stand, land + stand, trim );
Wedge( canvas, ramp.Wall, ramp.Foot, across, -half + kerb, -half + bite, top - bite, land - bite, top + stand, land + stand, trim );
}
static void SplineRamp( ArchMesh canvas, ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style )
{
var curve = SplineCurve( approach, kit );
var frames = curve.Walk( MathF.Max( 8f, kit.GridSize * 0.5f ) );
if ( frames.Count < 2 )
{
return;
}
var chain = new[] { approach.Palette, room.Palette, building.Palette };
var deck = style.Brush( ArchSurface.Deck, chain );
var bottom = MathF.Min( frames[0].Position.z, frames[^1].Position.z ) - MathF.Max( 1f, kit.RampThickness );
var rows = new List<Vector3[]>();
foreach ( var frame in frames )
{
var half = MathF.Max( 12f, approach.Width * frame.WidthScale ) * 0.5f;
rows.Add( new[] { frame.Side( -half ), frame.Side( half ) } );
}
ArchMeshSweep.Solid( canvas, rows, frames, kit.RampThickness, new[] { deck }, deck, false, soffit: _ => bottom );
if ( !approach.Kerbs )
{
return;
}
var trim = style.Brush( ArchSurface.Baseboard, chain );
var kerb = MathF.Max( 1f, kit.RampKerbWidth );
var stand = MathF.Max( 1f, kit.RampKerbHeight );
var bite = ArchContact.Bite( kit );
foreach ( var right in new[] { false, true } )
{
var edgeRows = new List<Vector3[]>();
foreach ( var frame in frames )
{
var half = MathF.Max( 12f, approach.Width * frame.WidthScale ) * 0.5f;
var outside = right ? half - bite : -half + bite;
var inside = right ? half - kerb : -half + kerb;
edgeRows.Add( new[]
{
frame.Side( inside ) + Vector3.Up * stand,
frame.Side( outside ) + Vector3.Up * stand
} );
}
ArchMeshSweep.Solid( canvas, edgeRows, frames, stand + bite, new[] { trim }, trim, false );
}
}
public static ArchCurve SplineCurve( ArchApproachPart approach, ArchKit kit )
{
var nodes = approach.Nodes.Select( node => node.Copy() ).ToList();
if ( nodes.Count < 2 )
{
return ArchCurve.Of( nodes );
}
var outward = ArchApproachAxes.Outward( approach );
var first = nodes[0];
var embedded = first.Position - new Vector3( outward.x, outward.y, 0f ) * Embed( kit );
first.Position = ArchGridService.Fine( embedded );
return ArchCurve.Of( nodes );
}
public static List<ArchRampRun> SplineRuns( ArchApproachPart approach, ArchRoom room, ArchBuilding building, ArchKit kit, ArchPlan plan = null )
{
var runs = new List<ArchRampRun>();
if ( !approach.IsSpline )
{
return runs;
}
var frames = SplineCurve( approach, kit ).Walk( MathF.Max( 8f, kit.GridSize * 0.5f ) );
var grade = Grade( room, building, kit, plan );
for ( var index = 0; index < frames.Count - 1; index++ )
{
var from = frames[index];
var to = frames[index + 1];
runs.Add( new ArchRampRun
{
Wall = from.Flat,
Foot = to.Flat,
WallTop = from.Position.z,
FootTop = to.Position.z,
Grade = grade,
HalfWidth = MathF.Max(
MathF.Max( 12f, approach.Width * from.WidthScale ),
MathF.Max( 12f, approach.Width * to.WidthScale ) ) * 0.5f
} );
}
return runs;
}
// Both ends take their own heights: a kerb rakes underneath as well as on top.
static void Wedge(
ArchMesh canvas,
Vector2 wall,
Vector2 foot,
Vector2 across,
float nearSide,
float farSide,
float wallBottom,
float footBottom,
float wallTop,
float footTop,
ArchBrush brush )
{
if ( MathF.Abs( nearSide - farSide ) < 0.05f || wallTop - wallBottom < 0.05f )
{
return;
}
var other = farSide;
var lower = new List<Vector3>
{
Corner( wall, across, nearSide, wallBottom ),
Corner( foot, across, nearSide, footBottom ),
Corner( foot, across, other, footBottom ),
Corner( wall, across, other, wallBottom )
};
var upper = new List<Vector3>
{
Corner( wall, across, nearSide, wallTop ),
Corner( foot, across, nearSide, footTop ),
Corner( foot, across, other, footTop ),
Corner( wall, across, other, wallTop )
};
canvas.Prism( lower, upper, brush );
}
static Vector3 Corner( Vector2 along, Vector2 across, float side, float height )
{
var flat = along + across * side;
return new Vector3( flat.x, flat.y, height );
}
}