Editor/Walkway/ArchWalkway.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchWalkway {
public const float MinWidth = 36f;
internal const float MinSpan = 48f;
const string FlightName = "WalkwayFlight";
public sealed class Link {
public ArchRoom Room { get; init; }
public ArchRoofPart Roof { get; init; }
public ArchBuilding Host { get; init; }
public IReadOnlyList<ArchWalkwayEndpoint> Endpoints { get; init; } = Array.Empty<ArchWalkwayEndpoint>();
public int Breached { get; init; }
public bool Rising { get; init; }
}
public static Link Add(
ArchPlan plan,
ArchBuilding building,
ArchKit kit,
int level,
float baseHeight,
Vector2 dragMin,
Vector2 dragMax,
Vector2 dragFrom,
float width,
float height,
ArchPillarPart pier,
int rise,
WalkwayInterior interior,
bool cornice,
bool skirting ) {
return new ArchWalkwayBuilder( plan, building, kit )
.Between( level, baseHeight, dragMin, dragMax, dragFrom )
.WithWidth( width )
.WithHeight( height )
.WithPiers( pier )
.WithRise( rise )
.WithInterior( interior )
.WithCornice( cornice )
.WithSkirting( skirting )
.Create();
}
// Deck ownership is on the layer record; falls back to footprint match for legacy links
public static ArchRoofPart Deck( ArchPlan plan, ArchRoom link ) {
if ( link is null ) {
return null;
}
var decks = plan.Buildings.SelectMany( building => building.Roofs ).Where( roof => roof.Walkway ).ToList();
var owned = plan.Layers
.Where( record => record.ParentId == link.Id )
.Select( record => decks.FirstOrDefault( roof => roof.Id == record.ItemId ) )
.FirstOrDefault( roof => roof is not null );
if ( owned is not null ) {
return owned;
}
var loop = ArchFloorGen.Footprint( link );
if ( loop.Count < 3 ) {
return null;
}
ArchFootprint.Bounds( loop, out var min, out var max );
return decks.FirstOrDefault( roof => roof.Level == link.Floor
&& (roof.Min - min).Length < 8f && (roof.Max - max).Length < 8f );
}
public static void Reshape( ArchPlan plan, ArchRoom link, IReadOnlyList<Vector2> outline, ArchKit kit = null ) {
var loop = ArchFloorGen.Footprint( link );
if ( loop.Count < 3 || outline is not { Count: >= 3 } ) {
return;
}
ArchFootprint.Bounds( loop, out var fromMin, out var fromMax );
ArchFootprint.Bounds( outline, out var toMin, out var toMax );
if ( !Passable( toMin, toMax ) ) {
return;
}
var span = Resolved( plan, link, kit, ref toMin, ref toMax );
var map = ArchRemap.Between( fromMin, fromMax, toMin, toMax );
outline = ArchFootprint.Rect( toMin, toMax );
ArchCarry.Room( link, map );
link.Footprint = ArchFootprint.Authored( outline ).Loop;
if ( span is not null ) {
SeatSides( link, span );
}
if ( Deck( plan, link ) is not { } deck ) {
return;
}
// Re-seated from the span, not remapped — deck stops on centrelines, slab on inner faces
if ( span is not null ) {
deck.Reshape( ArchFootprint.Rect( span.DeckMin, span.DeckMax ) );
return;
}
var covered = deck.Outline();
map.Loop( covered );
deck.Reshape( covered );
}
// Single source of truth for which axis is the run
public static bool RunsAlongX( Vector2 min, Vector2 max ) => max.x - min.x >= max.y - min.y;
static bool Passable( Vector2 min, Vector2 max ) {
var alongX = RunsAlongX( min, max );
return (alongX ? max.y - min.y : max.x - min.x) >= MinWidth
&& (alongX ? max.x - min.x : max.y - min.y) >= MinSpan;
}
// Reseats onto host walls; no kit means the drag stands as-is
static ArchWalkwaySpan Resolved( ArchPlan plan, ArchRoom link, ArchKit kit, ref Vector2 min, ref Vector2 max ) {
if ( kit is null ) {
return null;
}
var rise = link.WalkwayTop > link.BaseHeight + 1f ? 1 : 0;
var span = new ArchWalkwayGeometryService( new ArchConnections( plan, kit ) )
.Reseat( link.Floor, rise, min, max, link );
if ( span is null ) {
return null;
}
min = span.Min;
max = span.Max;
return span;
}
// Sides stop on centrelines, not inner faces — re-seated like the deck
internal static void SeatSides( ArchRoom link, ArchWalkwaySpan span ) {
var from = span.AlongX ? span.DeckMin.x : span.DeckMin.y;
var to = span.AlongX ? span.DeckMax.x : span.DeckMax.y;
foreach ( var wall in link.Walls ) {
var runs = span.AlongX ? MathF.Abs( wall.Direction.x ) : MathF.Abs( wall.Direction.y );
if ( runs < 0.7f ) {
continue;
}
var forward = span.AlongX ? wall.End.x >= wall.Start.x : wall.End.y >= wall.Start.y;
wall.Start = Along( wall.Start, span.AlongX, forward ? from : to );
wall.End = Along( wall.End, span.AlongX, forward ? to : from );
}
}
static Vector2 Along( Vector2 point, bool alongX, float value ) {
return alongX ? new Vector2( value, point.y ) : new Vector2( point.x, value );
}
public static void Raise( ArchPlan plan, ArchRoom link, ArchKit kit, float baseHeight ) {
var lift = baseHeight - link.BaseHeight;
if ( MathF.Abs( lift ) < 0.01f ) {
return;
}
link.BaseHeight = baseHeight;
if ( link.WalkwayTop > 0f ) {
link.WalkwayTop += lift;
}
foreach ( var stair in link.Stairs ) {
stair.BaseHeight += lift;
}
foreach ( var pier in link.Pillars ) {
pier.Height = MathF.Max( 1f, pier.Height + lift );
}
if ( Deck( plan, link ) is { } deck ) {
deck.BaseHeight += lift;
}
}
public static void Reheight( ArchRoom room, ArchRoofPart deck, ArchKit kit, float height ) {
var clear = MathF.Max( 60f, height );
var standing = ArchLap.Bury( kit, clear, -1f );
room.WallHeight = standing;
foreach ( var wall in room.Walls ) {
wall.Height = standing;
}
if ( deck is not null ) {
deck.BaseHeight = room.BaseHeight + clear;
}
foreach ( var pier in room.Pillars ) {
pier.Height = RiseIntoSlabFrom( room, kit, pier.BaseHeight );
}
}
static float RiseIntoSlabFrom( ArchRoom room, ArchKit kit, float from ) {
return ArchLap.Bury( kit, room.BaseHeight - kit.FloorThickness, -1f ) - from;
}
public static void SetWidth( ArchPlan plan, ArchRoom link, ArchKit kit, float width ) {
var loop = ArchFloorGen.Footprint( link );
if ( loop.Count < 3 ) {
return;
}
ArchFootprint.Bounds( loop, out var min, out var max );
var alongX = RunsAlongX( min, max );
var centre = (min + max) * 0.5f;
var half = MathF.Max( MinWidth, width ) * 0.5f;
Reshape( plan, link, ArchFootprint.Rect(
alongX ? new Vector2( min.x, centre.y - half ) : new Vector2( centre.x - half, min.y ),
alongX ? new Vector2( max.x, centre.y + half ) : new Vector2( centre.x + half, max.y ) ), kit );
}
public static float WidthOf( ArchRoom link ) {
var loop = ArchFloorGen.Footprint( link );
if ( loop.Count < 3 ) {
return 0f;
}
ArchFootprint.Bounds( loop, out var min, out var max );
return RunsAlongX( min, max ) ? max.y - min.y : max.x - min.x;
}
internal const float PierClearance = 12f;
// Asked here so the sidebar toggle and the service agree
public static bool Raised( ArchRoom link, ArchKit kit ) {
return link is not null && link.BaseHeight - kit.GroundClearance >= PierClearance;
}
public static void SetPiers( ArchPlan plan, ArchRoom link, ArchKit kit, ArchPillarPart source ) {
var kinds = ArchKinds.Load();
plan.RemoveAll( ArchKind.Pillar, link,
pillar => kinds.NameOf( ArchKind.Pillar, pillar, 0 ) == ArchWalkwaySupportService.PartName, kinds );
if ( source is null ) {
return;
}
ArchFootprint.Bounds( ArchFloorGen.Footprint( link ), out var min, out var max );
new ArchWalkwaySupportService( plan, plan.OwnerOf( link ), link, kit )
.AddPiers( source, min, max, RunsAlongX( min, max ) );
}
public static void SetInterior( ArchPlan plan, ArchRoom room, ArchKit kit, WalkwayInterior interior ) {
var kinds = ArchKinds.Load();
room.Interior = interior;
plan.RemoveAll( ArchKind.Stair, room, stair => kinds.NameOf( ArchKind.Stair, stair, 0 ) == FlightName, kinds );
if ( interior != WalkwayInterior.Stairs || room.Footprint.Count < 4 ) {
return;
}
ArchFootprint.Bounds( room.Footprint, out var min, out var max );
var alongX = (max - min).x >= (max - min).y;
var half = kit.WallThickness * 0.5f;
var width = (alongX ? max.y - min.y : max.x - min.x) - kit.WallThickness;
var run = alongX ? max.x - min.x : max.y - min.y;
var rise = MathF.Max( 1f, room.WalkwayTop - room.BaseHeight );
var steps = Math.Max( 1, (int)MathF.Round( rise / MathF.Max( 3f, kit.StepRise ) ) );
var going = MathF.Min( MathF.Max( 6f, kit.StepGoing ), MathF.Max( 24f, run * 0.45f / steps ) );
var flight = new ArchStairPart {
Id = plan.AllocateId(),
Name = FlightName,
BaseHeight = room.BaseHeight,
Width = MathF.Max( 24f, width ),
StepRise = rise / steps,
StepGoing = going,
TopLanding = false,
WellGuard = false,
Guard = StairGuard.None,
Under = StairUnder.Filled,
TreadThickness = 2.5f
};
var start = alongX
? new Vector2( min.x, min.y + half )
: new Vector2( max.x - half, min.y );
var end = alongX ? new Vector2( max.x, start.y ) : new Vector2( start.x, max.y );
var (core, lanes) = ArchStairCore.Straight( start, end - start, (end - start).Length, flight.Width, rise );
flight.Core = core;
flight.Lanes = lanes;
plan.File( ArchKind.Stair, room, flight, kinds );
}
public static void Shape( Vector2 dragMin, Vector2 dragMax, float width, out Vector2 min, out Vector2 max, out bool alongX ) {
alongX = dragMax.x - dragMin.x >= dragMax.y - dragMin.y;
var dragged = alongX ? dragMax.y - dragMin.y : dragMax.x - dragMin.x;
var across = dragged >= MinWidth ? dragged : MathF.Max( MinWidth, width );
var centre = (dragMin + dragMax) * 0.5f;
var half = across * 0.5f;
min = alongX ? new Vector2( dragMin.x, centre.y - half ) : new Vector2( centre.x - half, dragMin.y );
max = alongX ? new Vector2( dragMax.x, centre.y + half ) : new Vector2( centre.x + half, dragMax.y );
}
}