Editor/Stair/ArchApproachPlacement.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly record struct ArchApproachDrag( ArchApproachPart Placed, ArchRoom Room, ArchBuilding On, string Refused ) {
public bool Stands => Placed is not null;
}
public static class ArchApproachPlacement {
public static ArchApproachDrag Dragged(
ArchPlan plan, ArchKit kit, int level, Vector2 from, Vector2 to,
ApproachKind kind, float width, bool kerbs, bool rails, float splay,
ApproachAgainst against = ApproachAgainst.Auto ) {
var centre = (from + to) * 0.5f;
var buildings = (plan?.Buildings ?? Enumerable.Empty<ArchBuilding>()).ToList();
ArchWall wall = null;
ArchBuilding building = null;
ArchRoom room = null;
var along = 0f;
var toWall = float.MaxValue;
var outward = Vector2.Zero;
if ( against != ApproachAgainst.Platform ) {
wall = NearestExteriorWall( buildings, level, centre,
out building, out room, out along, out toWall, out outward );
}
var seat = against == ApproachAgainst.Foundation ? default : NearestPlatformEdge( buildings, level, centre );
if ( seat.Stands && (wall is null || seat.Gap <= toWall) ) {
return Onto( kit, seat, from, to, kind, width, kerbs, rails, splay, against );
}
if ( wall is null || room is null ) {
return new ArchApproachDrag( null, null, null, against == ApproachAgainst.Platform
? $"no platform on floor {level} — switch back to the foundation, or stand a podium to drag onto"
: $"no exterior wall on floor {level} — an approach comes off a building, so drag near one" );
}
var grid = new ArchGridService();
var drag = to - from;
var minimum = grid.SubgridSize( 4 );
var run = MathF.Abs( Vector2.Dot( drag, outward ) );
var span = Spanned( grid, wall.Length,
Vector2.Dot( from - wall.Start, wall.Direction ),
Vector2.Dot( to - wall.Start, wall.Direction ), width );
var placed = Seat( wall, room, building, kit, span.Station, kind, span.Width,
run < minimum ? 0f : run,
kerbs, rails, splay );
placed.Against = against;
return new ArchApproachDrag( placed, room, building, null );
}
// Ends snap flush to host corners within a cell; width is the span between them
public readonly record struct ArchApproachSpan( float Width, float Station );
static ArchApproachSpan Spanned( ArchGridService grid, float length, float lo, float hi, float typed ) {
var unit = grid.SubgridSize( 4 );
var cell = grid.BaseSize;
var run = MathF.Max( unit, length );
if ( hi < lo ) {
(lo, hi) = (hi, lo);
}
lo = Math.Clamp( lo, 0f, run );
hi = Math.Clamp( hi, 0f, run );
if ( hi - lo < unit ) {
var wide = Math.Clamp( grid.Subgrid( MathF.Max( unit, typed ), 4 ), unit, run );
return new ArchApproachSpan( wide,
Math.Clamp( grid.Subgrid( (lo + hi) * 0.5f, 4 ), wide * 0.5f, run - wide * 0.5f ) );
}
lo = lo <= cell ? ArchLap.Shy : grid.Subgrid( lo, 4 );
hi = run - hi <= cell ? run - ArchLap.Shy : grid.Subgrid( hi, 4 );
return new ArchApproachSpan( MathF.Max( unit, hi - lo ), (lo + hi) * 0.5f );
}
public readonly record struct ArchPodiumEdge(
ArchBuilding On, ArchRoom Room, ArchPlatformPart Platform,
Vector2 Start, Vector2 Along, float Length, Vector2 Outward, float Station, float Gap ) {
public bool Stands => Platform is not null;
}
// Outward is probed, not derived from winding — works regardless of loop direction
public static ArchPodiumEdge NearestPlatformEdge( IEnumerable<ArchBuilding> buildings, int level, Vector2 point ) {
var best = default( ArchPodiumEdge );
var nearest = float.MaxValue;
foreach ( var building in buildings ) {
var room = building.Rooms.FirstOrDefault( standing => standing.Floor == level && ArchLayerGate.On( standing ) )
?? building.Rooms.FirstOrDefault( standing => ArchLayerGate.On( standing ) );
foreach ( var platform in ArchLayerGate.Enabled( building.Platforms ) ) {
if ( platform.Level != level ) {
continue;
}
var outline = platform.Outline();
for ( var index = 0; index < outline.Count; index++ ) {
var start = outline[index];
var span = outline[(index + 1) % outline.Count] - start;
var length = span.Length;
if ( length < 1f ) {
continue;
}
var along = span / length;
var station = Math.Clamp( Vector2.Dot( point - start, along ), 0f, length );
var gap = (point - (start + along * station)).Length;
if ( gap >= nearest ) {
continue;
}
nearest = gap;
best = new ArchPodiumEdge( building, room, platform, start, along, length,
Off( outline, start + along * length * 0.5f, new Vector2( -along.y, along.x ) ),
station, gap );
}
}
}
return best;
}
static Vector2 Off( IReadOnlyList<Vector2> outline, Vector2 middle, Vector2 normal ) {
return ArchFootprint.Contains( outline, middle + normal ) ? -normal : normal;
}
static ArchApproachDrag Onto(
ArchKit kit, ArchPodiumEdge seat, Vector2 from, Vector2 to,
ApproachKind kind, float width, bool kerbs, bool rails, float splay, ApproachAgainst against ) {
var grid = new ArchGridService();
var unit = grid.SubgridSize( 4 );
var drag = to - from;
var run = MathF.Abs( Vector2.Dot( drag, seat.Outward ) );
var span = Spanned( grid, seat.Length,
Vector2.Dot( from - seat.Start, seat.Along ),
Vector2.Dot( to - seat.Start, seat.Along ), width );
var placed = new ArchApproachPart {
Kind = kind,
// Podium outline IS its face — no half-thickness offset
Origin = seat.Start + seat.Along * span.Station,
Yaw = MathF.Atan2( seat.Outward.y, seat.Outward.x ).RadianToDegree(),
Width = span.Width,
Run = run > unit ? grid.Subgrid( run, 4 ) : 0f,
Kerbs = kerbs,
Rails = rails,
Splay = splay,
Against = against,
PlatformId = seat.Platform.Id
};
return new ArchApproachDrag( placed, seat.Room, seat.On, null );
}
public static ArchApproachPart Seat(
ArchWall wall,
ArchRoom room,
ArchBuilding building,
ArchKit kit,
float along,
ApproachKind kind,
float width,
float run,
bool kerbs,
bool rails,
float splay ) {
var grid = new ArchGridService();
var unit = grid.SubgridSize( 4 );
var maximumWidth = MathF.Max( unit, wall.Length );
var snappedWidth = Math.Clamp( MathF.Max( unit, width ), unit, maximumWidth );
var centreAlong = Math.Clamp( along, snappedWidth * 0.5f, MathF.Max( snappedWidth * 0.5f, wall.Length - snappedWidth * 0.5f ) );
var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
var outward = ArchWallFaces.Outward( wall, room, building );
return new ArchApproachPart {
Kind = kind,
Origin = wall.PointAt( centreAlong ) + outward * (thickness * 0.5f + MathF.Max( 0f, kit.FoundationOversize )),
Yaw = MathF.Atan2( outward.y, outward.x ).RadianToDegree(),
Width = snappedWidth,
Run = run > 0f ? MathF.Max( unit, grid.Subgrid( run, 4 ) ) : 0f,
Kerbs = kerbs,
Rails = rails,
Splay = splay
};
}
public static ArchWall NearestExteriorWall(
IEnumerable<ArchBuilding> buildings,
int level,
Vector2 point,
out ArchBuilding building,
out ArchRoom room,
out float along,
out float distance,
out Vector2 outward ) {
ArchWall best = null;
building = null;
room = null;
along = 0f;
distance = float.MaxValue;
outward = default;
foreach ( var candidateBuilding in buildings ) {
foreach ( var candidateRoom in candidateBuilding.Rooms ) {
if ( candidateRoom.Floor != level ) {
continue;
}
foreach ( var candidateWall in candidateRoom.Walls ) {
var length = candidateWall.Length;
if ( length < 0.5f || !ArchWallFaces.TryOutward( candidateWall, candidateRoom, candidateBuilding, out var candidateOutward ) ) {
continue;
}
var projected = Math.Clamp( Vector2.Dot( point - candidateWall.Start, candidateWall.Direction ), 0f, length );
var gap = (point - candidateWall.PointAt( projected )).Length;
if ( gap >= distance ) {
continue;
}
best = candidateWall;
building = candidateBuilding;
room = candidateRoom;
along = projected;
distance = gap;
outward = candidateOutward;
}
}
}
return best;
}
}