Editor service that computes placement and editing geometry for walkway (arch) links between rooms. It resolves the gap left by room shells, determines end rooms, mouths, deck extents and returns an ArchWalkwaySpan for placing or reseating a walkway.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchWalkwayGeometryService
{
readonly ArchConnectionService connections;
public ArchWalkwayGeometryService( ArchConnectionService connections )
{
this.connections = connections;
}
// PLACEMENT: the drag is a proposal, and where the link fits is the gap left over once the
// buildings are subtracted out of it. ignore is the link being re-dragged: it already stands in
// the gap, so subtracting its own footprint would leave no gap to span and answer nothing.
public ArchWalkwaySpan Resolve( int level, int rise, Vector2 dragFrom, Vector2 dragMin, Vector2 dragMax, float width, ArchRoom ignore = null )
{
ArchWalkway.Shape( dragMin, dragMax, width, out var shapedMin, out var shapedMax, out var alongX );
var shells = connections.Storey( level )
.Concat( connections.Storey( level + rise ) )
.Where( room => !ReferenceEquals( room, ignore ) && !room.Spans )
.Select( room => ArchFloorGen.Footprint( room ) )
.Where( loop => loop.Count >= 3 && ArchFootprint.IsRectilinear( loop ) )
.ToList();
var bounds = ResolveGap( shapedMin, shapedMax, ArchRegion.Shell( shells, connections.Kit.WallThickness ), alongX );
return bounds is null ? null : Seat( level, rise, dragFrom, bounds.Value.Min, bounds.Value.Max, alongX, ignore );
}
// EDITING: the rectangle the handles left IS the span. Neither the gap search nor Shape's
// width-filling runs here, because both are answers to where a NEW link goes - the search hands
// back the gap between the shells whatever was dragged, and the filling CENTRES the passage on the
// drag, which slid the whole link sideways every time one side was pulled in. Only the ends are
// re-seated, so one pulled onto a host still lands on its inner face and one pulled clear of every
// host stands where it was put.
public ArchWalkwaySpan Reseat( int level, int rise, Vector2 min, Vector2 max, ArchRoom link )
{
return Seat( level, rise, min, min, max, ArchWalkway.RunsAlongX( min, max ), link );
}
// One seating for both, or a reshape and a placement disagree about where the same end lands.
ArchWalkwaySpan Seat( int level, int rise, Vector2 dragFrom, Vector2 min, Vector2 max, bool alongX, ArchRoom ignore )
{
if ( Reach( min, max, alongX ) < ArchWalkway.MinSpan )
{
return null;
}
// Each end runs into the wall its mouth cuts: the room's footprint edge is the wall's
// centreline, so half a wall in puts the end on the wall's inner face - the link's own
// walls then meet the room's inner faces at the mouth corners.
var half = connections.Kit.WallThickness * 0.5f;
var reach = connections.Kit.WallThickness * 2f;
var centre = (min + max) * 0.5f;
// The drag starts at the low end: that side opens at the current storey, the far side climbs.
var lowAtMin = alongX ? dragFrom.x <= centre.x : dragFrom.y <= centre.y;
var west = ResolveEnd( level, rise, lowAtMin, alongX ? new Vector2( min.x, centre.y ) : new Vector2( centre.x, min.y ), reach, ignore );
var east = ResolveEnd( level, rise, !lowAtMin, alongX ? new Vector2( max.x, centre.y ) : new Vector2( centre.x, max.y ), reach, ignore );
// The slab runs on to the host wall's INNER face so it meets that room's own slab with no seam at
// the threshold. The deck stops on the CENTRELINE, the way every deck does, and lets the wall
// cover the rest - run it to the inner face too and its edge surfaces on the room side of that
// wall, which is the ceiling piercing it. Both extents are answered HERE, because every reshape
// re-derives from this one resolution and a deck trimmed after the fact is one the next drag untrims.
var deckFrom = alongX ? min.x : min.y;
var deckTo = alongX ? max.x : max.y;
if ( west is not null && west.HasFootprint )
{
ArchFootprint.Bounds( ArchFloorGen.Footprint( west ), out _, out var roomMax );
deckFrom = alongX ? roomMax.x : roomMax.y;
min = alongX ? new Vector2( deckFrom - half, min.y ) : new Vector2( min.x, deckFrom - half );
}
if ( east is not null && east.HasFootprint )
{
ArchFootprint.Bounds( ArchFloorGen.Footprint( east ), out var roomMin, out _ );
deckTo = alongX ? roomMin.x : roomMin.y;
max = alongX ? new Vector2( deckTo + half, max.y ) : new Vector2( max.x, deckTo + half );
}
var deckMin = alongX ? new Vector2( deckFrom, min.y ) : new Vector2( min.x, deckFrom );
var deckMax = alongX ? new Vector2( deckTo, max.y ) : new Vector2( max.x, deckTo );
// The mouths open at the passage's width - the walkway's own walls are the jambs.
var mouths = alongX
? new[] { (new Vector2( min.x, min.y + half ), new Vector2( min.x, max.y - half )), (new Vector2( max.x, max.y - half ), new Vector2( max.x, min.y + half )) }
: new[] { (new Vector2( max.x - half, min.y ), new Vector2( min.x + half, min.y )), (new Vector2( min.x + half, max.y ), new Vector2( max.x - half, max.y )) };
return new ArchWalkwaySpan { Min = min, Max = max, DeckMin = deckMin, DeckMax = deckMax, AlongX = alongX, Mouths = mouths, EndRooms = new[] { west, east } };
}
// The low end (where the drag started) opens at the current storey; the far end climbs to it.
// Never the link itself: its own end stands on its own boundary, so an end pulled clear of every
// host would otherwise seat on the link it belongs to and take its extent from that.
ArchRoom ResolveEnd( int level, int rise, bool lowEnd, Vector2 point, float reach, ArchRoom ignore )
{
if ( !lowEnd && rise > 0 )
{
return connections.RoomNear( level + rise, point, reach, ignore )
?? connections.RoomNear( level, point, reach, ignore );
}
return connections.RoomNear( level, point, reach, ignore )
?? connections.RoomNear( level + rise, point, reach, ignore );
}
static (Vector2 Min, Vector2 Max)? ResolveGap( Vector2 dragMin, Vector2 dragMax, IReadOnlyList<List<Vector2>> shells, bool alongX )
{
if ( shells.Count == 0 )
{
return (dragMin, dragMax);
}
// Buildings subtracted out of the drag rectangle: the walkway spans the gap left between them.
var gap = ArchRegion.Minus( ArchFootprint.Rect( dragMin, dragMax ), shells );
var longest = ArchFootprint.Outer( gap )
.OrderByDescending( loop => Extent( loop, alongX ) )
.FirstOrDefault();
if ( longest is null )
{
return null;
}
ArchFootprint.Bounds( longest, out var min, out var max );
return (min, max);
}
static float Extent( IReadOnlyList<Vector2> loop, bool alongX )
{
ArchFootprint.Bounds( loop, out var min, out var max );
return Reach( min, max, alongX );
}
static float Reach( Vector2 min, Vector2 max, bool alongX )
{
return alongX ? max.x - min.x : max.y - min.y;
}
}