Editor/Walkway/ArchWalkwayGeometryService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchWalkwayGeometryService {
readonly ArchConnections connections;
public ArchWalkwayGeometryService( ArchConnections connections ) {
this.connections = connections;
}
// ignore excludes the link being re-dragged so its own footprint doesn't close the gap it spans.
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.Bearing( loop, out _ ) )
.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 reseats ends only — gap search and width-filling are placement-only answers.
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;
}
// Half a wall in seats each end on the host's inner face.
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 );
// Slab to inner face (meets host slab), deck to centreline (wall covers the rest).
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;
}
}